pronto.ee

Tomorrow will be cancelled due to lack of interest

PHP8: tüübimaagia

PHP

PHP8 tõi sisse hulgaliselt täiendusi tüüpidega majandamisesse ning PHP8.1 lisas sinna omakorda peale täiendava kihi. Selles postituses keskendume me siiski vaid PHP8 muudatustel.

PHP on n.ö. nõrkade tüüpidega (weak typing) keel, mis praktikast tähendab seda, et erinevaid tüüpe on võimalik võrrelda ja nende väärtuseid teineteisele omistada; selleks vajalik tüüpide kantimine (type coercion) toimub automaatselt. Teinekord paraku soovimatute kõrvalnähtudega. PHP8 üritab jõudumööda neid soovimatuid kõrvaltoimed kas siis vältida või vähemalt pakub võimaluse neid kontrolli all hoida.

Tüüpide võrdlemine

Klassikaline erinevus PHP7 ja PHP8 vahel ilmneb järgnevas võrdluses:

if ('something' == 0) {
    echo 'true';
} else {
    echo 'false';
}

Nii 'something' kui 0 võivad olla muutujad; PHP7 puhul on vastuseks true, PHP8 puhul false. Asi on selles, et kui võrdluses on int ja string, siis proovitakse stringist teha int. Kuna tegemist on mittenumbrilise tekstiga, siis PHP7 pani selle väärtuseks 0 sellal kui PHP8 jaoks väärtus puudub ja sellest ka erinev tulemus. Samasuguse tulemuse annab näites järgnev lõik:

if ('42something' == 42) {
    echo 'true';
} else {
    echo 'false';
}

Selline tüüpide kantimine on teinekord väga mugav, näiteks XML failide töötlemisel, kuid sisaldab endas riske ja tänu sellele võib juhtuda, et kood mis toimis PHP7-s ei tööta enam PHP8-s. Märkuseks nii paljud, et:

if ('42' == 42) {
    echo 'true';
} else {
    echo 'false;
}

… on true vaatamata PHP versioonile, kuna ’42’ on string mida on võimalik üheselt intiks kantida.

Muudatus tüüpide kantimises torkab silma ka siis kui kasutada < ja > võrdluseid. Kui varem kanditi string intiks ja seejärel võrreldi, siis nüüd kui stringi ei ole võimalik konvertida on number alati väiksem kui string. Need nüansid laienevad muudelegi võrdlustele, näiteks in_array käitub nüüd pisut erinevalt.

Ma siiski tooks enne järgmise näite juurde minekut eraldi välja erisuse kui kasutada switchi.

switch(0) {
    case 'a' : print "case A\n"; break;
    case 0   : print "case 0\n"; break;
}
 
switch('a') {
    case 'a' : print "case A\n"; break;
    case 0   : print "case 0\n"; break;
}
 
switch(0) {
    case 0   : print "case 0\n"; break;
    case 'a' : print "case A\n"; break;
}
 
switch('a') {
    case 0   : print "case 0\n"; break;
    case 'a' : print "case A\n"; break;
}

Annab PHP7 puhul tulemuseks

case A
case A
case 0
case 0

ja PHP 8 puhul

case 0
case A
case 0
case A

Mis on eelnevat arvestades igati loogiline ja enamasti ka soovitud tulemus. Ehk siis nõrkade tüüpidega manipuleerides võib juhtuda, et kood käitub pisut teisi.

Unionid

PHP7 tõi keelde funktsioonide pöördumistüübi ning PHP7.1 nullitavad tüübid (nullable types). Et vältida soovimatut tüüpide kantimist ning aidata IDE-si soovituste andmisel on funktsioonidel võimalik anda tüüp nii sisendi kui väljundi jaoks. Nullitavad tüübid (algavad ? märgiga) näitavad, et tüüp võib (näiteks kui vastus ei leita) olla ka null. Näide:

public function doStuff(int $i): ?string
{
    // stuff
}

Aga mis siis kui sisend (või väljund) võib olla rohkem kui ühte tüüpi? Iseenesest pole midagi valesti selles kui muutujaid kanditakse enne või pärast funktsiooni välja kutsumist kuid see on tülikas, lisab ballasti ja ei aita loetavusele kaasa. Sestap tõi PHP8 sisse unionid. Näide:

public function add(int|float $a, int|float $b): int|float
{
    return $a + $b;
}

Märkuseks niipalju, et null ei tegelikult tüüp, vaid tüübi puudumine ja sestap ei ole string|null endiselt korrektne konstruktsioon ja selle asemel tuleks kasutada ?string tüüpi.

mixed tüüp

Teinekord on vaja lihtsalt konstruktsiooni kus lubatud on kõik tüübid KAASA ARVATUD null. Selleks puhuks tõi PHP8 sisse pseudotüübi nimega mixed. Näiteks:

public function doStuff(mixed $input): bool
{
    // stuff
}

Muutujad mille tüüp pole veel selge tüüp on samuti mixed. Kuna mixed sisaldab kõiki tüüpe ei oma int|mixed ja ?mixed mingit tähendust ning annavad seetõttu vea. Samal põhjusel ei eksisteeri is_mixed funktsiooni ning samuti ei ole võimalik teha $b = (mixed)$a.

Üks asi veel: alatest PHP8-st ei ole enam võimalik deklareerida klassi nimega mixed. Kuni sinnamaani oli see teoreetiliselt võimalik.

PHP8: konstruktori parameetrite eskaleerimine

PHP

Konstruktori parameetrite eskaleerimine (Constructor Property Promotion) on tõenäoliselt kõige praktilisem uuendus mille PHP8 endaga kaasa tõi. Ühes dependency injectionite ja reflectionite järjest laialdasema kasutamisega näeb tavalise konstruktori signatuur koos parameetrite, muutujate ja omistustega välja umbes selline:

protected Database $database;
protected Config $config;
protected Source $source;

public function __construct(Database $database, Config $config, Source $source)
{
    $this->database = $database;
    $this->config = $config;
    $this->source = $source;

ning objekti enda initsialiseerimine selle klassi põhjal selline:

$adapter = new Adapter($database, $config, $source);

Keerukamate rakenduste puhul kasutavad objektid kümneid muid objekte eriti kui mängus on reflectionid mis initsialiseerimise poole suuresti automatiseerivad ja arendajat seoserägastikuga kaasnevast peavalust säästavad. Paraku tingib see samas olukorra kus injectioneid tehakse väga kergekäeliselt ning iga klassi konstruktoris on hulgaliselt parameetreid mis siis ükshaaval klassimuutujate külge kleebitakse. Ning tõsisemates raamistikes / rakendustes on klasse sadu kui mitte tuhandeid.

See on nüri, see võtab aega, see muudab koodi pikaks ja ühes sellega loetamatuks.

PHP8-s näeb analoogne kood välja selline:

public function __construct(protected Database $database, protected Config $config, protected Source $source)
{

Kui erandjuhud näiteks referentside (need & märgiga algavad muutujad) näol kõrvale jätta, on parameetrites deklareeritud muutujate skoop ja ühes sellega nähtavus alati piiratud funktsiooniga. Erandiks on PHP8-s konstruktor kus nähtavuse lisamine ütleb klassile, et nähtavusega muutujad tuleb automaatselt eskaleerida klassiüleseks ning nende nähtavus (private, protected, public) oleks mida iganes nende nähtavuseks määrati. Muutujad millel nähtavust pole jäävad endiselt vaid konstruktorile endale nähtavaks.

Nagu näha on see tükk maad lühem, loetavam ja suures plaanis on seal palju vähem ruumi vigadele. Seal on siiski mõned nüansid millega peab arvestama:

  1. Eskaleerivaid muutjaid saab deklareerida AINULT konstruktoris
  2. Nõnda deklareeritud muutujaid EI SAA uuesti (eraldi) deklareerida
  3. Variadic muutujaid EI SAA eskaleerida
  4. Callable tüüpi muutujaid EI SAA eskaleerida
  5. Abstract konstruktorid EI TOETA eskaleerimist.

Enamus neist ülaltoodud piirangutes on tegelikult äärmiselt loogilised ning kui nende üle mõelda aitavad mõista eskaleerimise olemust. Seega eskaleerigem mõnuga!

PHP8: nullikindel operaator

PHP

Vajadus nullikindla operaatori (null-safe operator) järele eksisteerib PHP-s juba mõnda aega. Põhimõtteliselt võimaldab see nüüd kasutada pöördumisväärtuste ahelat ilma täiendavate kontrollideta ka siis kui seda kasutatakse koos väärtustamata tüüpidega (nullable types).

Et sellest paremini aru saada tuleb vaadata mõlemat asja eraldi. Väärtustamata tüübid lisati PHP-sse hulk aastaid tagasi versioonis 7.1. Klassikaline väärtustamata tüübiga meetodi signatuur näeb välja selline:

public function getSession(): ?Session
{
    // Code
}

Praktikas tähendab see seda, et kirjeldatud funktsioonil võib olla kahte tüüpi pöördumisväärtus: Session või null. Null on spetsiifiline tüüp mida kasutatakse sellistel puhkudel kui väärtust ei leita: näiteks antud juhul oleks täiesti legitiimne viis tagastada null kui sessioon on initsialiseerimata. Siinkohas on oluline mõista, et see on legitiimne viis ainult siis kui tõepoolest selline olukord on loomulik voo osa, näiteks sellisel puhul kui kasutaja pole sisse loginud. Kui sessiooni ei ole ühel või teisel (tehnilisel) põhjusel võimalik pärida, siis on korrektseks lahenduseks Exceptioni viskamine. See võimaldab vahet teha vastuse puudumisel ja veal.

Tähelepanuks niipalju, et väärtustamata tüüpe võib kasutada ka sisendaväärtustena. Näiteks:

$user->setAddress(?Address $address);

Tüüpide kasutamine pole tänapäeva PHP-s küll otseselt kohustuslik kuid sellele vaatamata rangelt soovituslik; see võimaldab IDE-del automaatselt tuvastada koodis vigu ning vältida olukorda kus funktsioonile antakse ette vale parameeter mille PHP automaatselt ootamatute tagajärgedega enda jaoks sobivaks konverteerib.

Pöördumisväärtuste ahel on PHP veelgi kauem eksisteerinud. Klassikaline ahel näeb välja selline:

$country = $app->getSession()->getUser()->getAddress()->getCountry();

See võimaldab juhul kui pöördumisväärtuseks on objekt selle meetodeid koheselt kasutada. Probleem tekib siis kui mõne meetodi pöördumisväärtuseks on null. Sellistel puhkudel on tulemuseks: Fatal error: Uncaught Error: Call to a member function getSession() on null in .... Kuna eelnevalt sai toonitatud, et null võib olla meetodil täiesti legitiimne pöördumisväärtus sunnib see arendajat iga pöördumisväärtust eraldi kontrollima mis oma olemusel muudaks kogu kontseptsiooni kasutuks.

PHP8 tõi sellele probleemile lahenduse nullikindlate operaatorite (null-safe operators) näol. Praktikas näeb see välja selline:

$country = $app->getSession()?->getUser()?->getAddress()?->getCountry();

Sellisel puhul kui ükskõik milline ülaltoodud meetod annab tagasi null väärtuse, lõpetatakse ilma veata ahela edasine täitmine ning muutuja $country saab väärtuseks null. Selline lähenemine muudab koodi oluliselt lühemaks ja loetavamaks.

Oluline on siiski pidada silmas paari nüansse:

  1. ?-> tuleks kasutada ainult siis kui pöördumisväärtus võib reaalselt null olla. Vastasel korral on see probleemi vaiba serva alla lükkamine ehk siis reaalne viga võib arenduse käigus märkamata jääda.
  2. Seda saab kasuatada ainult lugemiseks. Muutujaid selle kaudu omistada ei saa. Näiteks $country()?->countryCode = 'EE'; annab tulemuseks Fatal error: Can't use nullsafe operator in write context in …
  3. See operaator annab varasemata PHP versioonidega vea: Parse error: syntax error, unexpected '->' (T_OBJECT_OPERATOR) in …
  4. Seda meetodit ei ole võimalike kasutada läbi viidete (references): &$country->getCode(); annab tulemuseks: Fatal error: Cannot take reference of a nullsafe chain in …

PHP8: match avaldis

PHP

Ma olen viimastel aastatel päris palju pidanud algajate arendajate koodi hindama ja üks asi mis mulle silma torkab on see kui harva kasutatakse switch-i tingimuste lahendamiseks. Selle asemel leiab koodist erineval kujul terve leegioni if, else ja elseif lauseid. Ma ei hakka sellest hetkel pikemalt pajatama; jätke lihtsalt meelde, et switch on teie sõber. Kui seda õigesti kasutada siis muudab see koodi loetavamaks ja lühemaks.

PHP8 tõi sisse uue viisi tingimustega tegelemiseks mis on segu switch-ist ja kolmekordsetest (ternary) operaatoritest. Kui ei tule kohe meelde mida kolmekordne operaator endast kujutab, siis tüüpiliselt näeb see välja selline:

$i = $condition ? 'true' : 'false';

Uue viisi nimi on match ja kui tingimused klapivad, siis muudab see koodi veelgi lühemaks ja veelgi loetavamaks. Pidage meeles, et iga rida koodi on midagi mida tuleb jooksvalt hooldada ja vajaduse korral uuendada. Kui loetavus sellest ei kannata, siis mida vähem koodi seda parem. Toon siinkohas ühe näite kasutades eelmises postituses mainitud anonüümseid funktsioone:

$animal = 'dog';

$voice = match($animal) {
    'dog' => function() {return 'woof';},
    'cat' => function() {return 'meow';},
    'poro' => function() {return 'perkele!';},
    default => function() {return 'huh?';}
};

echo $voice();

Siinkohas oleks paslik ära märkida paar nüanssi. Esiteks match eeldab, et muutuja $animal on deklareeritud. Kuigi ta selle peale tööd ei katkesta ning väärtustab tulemi default-iga, kuvab ta vaikimisi hoiatuse: Warning: Undefined variable $animal

Teiseks on oluline, et match-i parameeter ja väärtus millega võrreldakse ('dog', 'cat', jne.) oleks sama tüüpi. Näiteks kui võrrelda väärtuseid 1 ja '1', siis on tulemuseks default. See on sarnane PHP === võrdlusega, kus erinevaid tüüpe ei ühtlustata nagu tehakse == võrdluse puhul.

Kolmandaks PEAB valik sisaldama tõest väärtust (kasvõi default-i näol). Kui lahendit ei leita on tulemuseks: Fatal error: Uncaught UnhandledMatchError: Unhandled match value of type string in ...

Teinekord kasutavad arendajad analoogse tulemuse jaoks nimelisi massiive:

$animal = 'dog';
$voices = [
    'dog' => function() {return 'woof';},
    'cat' => function() {return 'meow';},
    'poro' => function() {return 'perkele!';}
];

echo $voices[$animal]();

Sellel on kaks eelist: esiteks nagu näha on see ilma kontrollideta veelgi lühem ja teiseks töötab see varajasemate PHP versioonidega. Samas ei toeta see default-i mis praktikas tähendab seda, et tuleb kontrollida kas sellise nimega element eksisteerib, funktsioonid deklareeritakse isegi siis kui neid ei kasutata (see ei ole probleem kui väärtuseks on skalaarmuutujad nagu int, float, string jne.) ja tüüpi ei kontrollita. Sellise lahenduse kasutamine on iseenesest samuti ok ehk siis valik tuleb teha lähtuvalt ülaltoodud piirangutest.

PHP8: create_function() eemaldati

PHP

Ma olen viimased kuu aega PHP8 sees ringi mütanud eesmärgiga aru saada mis sealt uut leiab ja kuidas sellest võimalikult palju kasu lõigata ning olen sealt leidnud päris mitu tähelepanu väärivad asjad. Püüan osadest neist paari sõnaga kirjutada.

Selle konkreetse postituse teemaks on create_function() nimeline funktsioon, õigemini selle puudumine PHP8-s. Algselt lisati see PHP4-le ning selle mõte oli tuua PHP-sse anonüümsed funktsioonid. PHP5-e viimastes versioonides muudeti see kõik juba keele osaks, kuid mingil seletamatul põhjusel jätkas suur hulk arendajaid selle funktsiooni kasutamist vaatamata sellele, et funktsiooni MITTE kasutamiseks oli päris mitu head põhjust; näiteks kasutab nõnda loodud funktsioon globaalset mälu ning seda ei ole võimalik vabastada. Kuna tegemist on jõhkra häkiga kuulutati see PHP 7.2-s eemaldamisele minevaks ning alates PHP8-st kustutati see üldse maha.

Mis omakorda tähendas seda, et kui PHP-s kirjutatud tarkvara kasutas seda funktsiooni tervitab selle kasutajat pärast uuendamist järgmine veateade: Uncaught Error: Call to undefined function create_function() in <fail>. Kuna pahatihti kasutasid seda WordPressi kujunduste ja moodulite kirjutajad tähendas see seda, et pärast PHP versiooni vahetamist loobus nii mõnigi veebisait koostööst. Eriti siis kui tegemist oli pikemat aega toiminud leheküljega ja vaatamata sellele, et platvorm ise oli viimase versiooni peal.

Õnneks on seda probleemi suhteliselt hõlbus parandada. Panen siia ülesse ühe näite. Kõigepeal näide mis PHP8-s EI tööta:

$function = create_function('$a,$b','return $a . \' \' . $b;');
echo $function('Hello', 'World!');

Ja nüüd näide mis töötab:

$function = function($a, $b) {return $a . ' ' . $b;};
echo $function('Hello', 'World!');

Nagu näha on teine näide loetavam, elegantsem ja võtab vähem ressursse. Ning selle juurutamine ei ole üldse keeruline.

Ehk on abiks.