Nevoia-i cel mai bun profesor – Câteva noutăți de la PHP 8.3 încoace

În dulcele stil clasic Te-am plătit o dată și trebuie să mă servești pentru totdeauna am primit un apel telefonic la capătul căruia era un om teribil de supărat cu acuzația directă că nu merge și trebuie să fac pe dracu-n patru să fie din nou brici. Și, deși uit multe lucruri, fiind mai degrabă căscat, două detalii reținute cu sfințenie sunt termenul de garanție și condițiile de acordare.

You've got the wrong woom

You’ve got the wrong woom

Drept consecință, vocea de la capătul telefonului a primit prima veste proastă: garanția expirase și, din câte îmi amintesc, nu avem un contract de mentenanță. Și, colac peste pupăză, eu doar i-am operat niște modificări punctuale pe ceva deja existent, deci nu pe mine m-a plătit grosul. Așadar, investigația costă atât, durează atât și în urma dânsei vom stabili și ce soluții sunt, cu costurile aferente de timp și marafeți.

Cum Ilie Iobăgie a intrat și la mine cu talpa-nainte, n-am chef de prea multe acte de caritate, deci la protestul repetat că m-a plătit să i-l fac și trebuie să meargănu merge, am salutat politicos și mi-am văzut de ale mele.

După desfășurarea de rigeur a micului dans balcanic, am primit cele mai dragi vorbe posibile, adică trimite factura și, după confirmarea încasării, mi-am aruncat nasul în ceea ce s-a dovedit o budă cu orori provocate de actualizarea la versiunea PHP 8.3.24.

Fiind conștient că nu o să plătească pentru corectarea propriu-zisă a incompatibilităților, i-am adus la cunoștință cauza și sugestia să revină la fosta versiune ce nu provoca dureri de cap și răceală-n buzunar, ceea ce s-a și-mplinit.

Pe de altă parte, a servit drept o bună și revigorantă sesiune de instructaj vis-a-vis de noutățile lumii Pe’așpeului, începând cu versiunea 8.3, iar ce urmează e o scurtă dare de seamă nu doar cu sursele de probleme găsite-n urma investigației, ci și altele găsite fie interesante, fie dubioase, fie ambele.

Clasele pot avea constante cu tip de date

Practic, începând cu PHP 8.3 se poate specifica un tip de date (int, string etc.) pentru o constantă definită într-o clasă, iar clasele ce ulterior o suprascriu trebuie să respecte acel tip, adică trebuie să fie ori identic, ori restrâns.

<?php
class Api {
    public const int VERSION = 1;
}

class NewApi extends Api {
    //PHP Fatal error:  Type of NewApi::VERSION must be compatible 
    //	with Api::VERSION of type int in constants.php 
    //	on line XYZ
    public const string VERSION = 'v2';
}

echo NewApi::VERSION . PHP_EOL;

Aceasta, în schimb, este o versiune corectă (a se vedea, însă, comentariul ulterior):

<?php
class Api {
    public const string|int VERSION = 1;
}

class NewApi extends Api {
    // Class constant narrowed down to string only
    public const string VERSION = 'v2';
}

echo NewApi::VERSION . PHP_EOL;

Pe de o parte, mi se pare binevenit; pe de altă parte… ori e constantă, ori nu mai e? Dacă ajungi în punctul în care îți pui asemenea probleme, adică de a suprascrie o constantă, poate că trebuie luat altceva în calcul, întrucât e limpede că nu mai e chiar atât de constantă. După cum se vede, este foarte lesnicios a continua să treci de la numere întregi la șiruri de caractere sau să le folosești pe ambele simultan, cu mențiunea că incertitudinea va fi măcar formală.

Aș mai puncta și faptul că PHP, pe lângă că-ncurajează abuzarea constantelor ca variabile, nu ajută foarte mult nici permițând includerea constantelor în semnătura unei interfețe, ușurând astfel munca necesară adăugării unor artefacte cu aromă de concretețe fără mare valoare într-un concept eo ipso abstract.

Verificarea corectă a vizibilității constantelor la implementarea unei interfețe

Acum una pișcătoare. Nu că explicitarea tipologiei constantelor nu ar fi avut potențialul de-a-nțepa, atâta doar că-i mai puțin probabil. Aici e invers: e posibil s-o dai, dar e probabil s-o iei. Pe scurt, înainte PHP 8.3, este permisă implementarea unei interfețe cu specificarea unui nivel mai redus al vizibiltății constantelor (ex. protected), chit că total contraintuitiv:

<?php 
interface Api { 
    public const VERSION = 1; 
} 

class NewApi implements Api { 
    protected const VERSION = 'v2'; 
    public static function getVersion() { 
        return self::VERSION; 
    } 
} 

echo NewApi::getVersion() . PHP_EOL;

N-am menționat degeaba-n deschidere că-i pișcătoare: a fost una din sursele problemelor ce-au dat cu ranga la gioalele aplicației în cauză. Dintr-un motiv, din câte-am putut să-mi dau seama, ținând de-o mai bună încapsulare, dezvoltatorul inițial implementase o interfață profitând de ciudata incosecvență a evaluării vizibilității constantelor unei clase (dacă-ncercați agerimea extinzând o altă clasă veți primi, așa cum se cuvine, o riglă la palmă). Drumul spre Iad…

Atributul #[\Override]

Nu, nu m-am îmbătat. Nu de curând, în orice caz. Nu, atributul se asigură că metoda căruia îi este aplicat chiar suprascrie o metodă din clasa extinsă (sau din interfața implementată). Din câte-mi pot da seama verifică doar corespondența de nume, adică trebuie să existe o metodă cu acel nume în ierarhia de la etaj.

<?php 
class Api {
    public function doStuff($a) {
        echo 'DO STUFF'. PHP_EOL;
    }
}

class NewApi extends Api {
    //PHP Fatal error:  NewApi::doStufff() has #[Override] attribute, 
    //	but no matching parent method exists 
    //	in override.php on line 13
    #[Override]
    public function doStufff() {
        echo 'DO NEW STUFF'. PHP_EOL;
    }
}  

$api = new NewApi();
$api->doStuff();

Într-un fel acționează similar cu perechea virtual/override din C#, diferența semnificativă fiind că trebuie să marchezi o metodă ca virtual înainte de-a o putea suprascrie, deci intența trebuie gândiră și declarată corespunzător de ambele părți, făcând de-un contract foarte clar.

Așadar, per total, cred că #[\Override] este un pas înainte.

Incrementarea și decrementarea șirurilor au niște aspecte căzute-n desuetudine

Pe scurt, o categorie întreagă de șiruri de caractere vor emite avertizări dacă-s folosite cu operatorii ++ și – –, inclusiv șirurile goale, non-numerice ori non-alfanumerice. Pe bună dreptate că atare utilizări au intrat în desuetudine oficială, emițând mesaje adecvate (mai puțin în cazul celor non-numerice).

Alături de cele generate de modificările descrise de capitolul următor, au stricat apele în câteva puncte unde se procesa conținutul emis în cadrul definit de un ob_start() / ob_get_clean(). Dintr-un motiv oarecare, probabil din neglijență, dezvoltatorul inițial uitase-n „producție” un error_reporting (E_ALL) în paginile respective, îmbogățind textul emis cu elemente noi și surprinzătoare.

Există o nouă pereche de funcții, str_increment, respectiv str_decrement, menite a înlocui cei doi operatori, amendând mai ferm abaterile de la forma cerută a șirului de intrare, anume emițând o exceție de tip ValueError atunci când utilizatorul nimerește alăturea.

<?php
error_reporting(E_ALL);

$empty = '';
//Works, but issues PHP Deprecated:  
//	Increment on non-alphanumeric string 
//	is deprecated in string_ops.php on line 9
$string2 = ++$empty;
var_dump($string2);

try {
    $empty = '';
    // Throws altogether: PHP Fatal error:  
    //	Uncaught ValueError: str_increment(): 
    //	Argument #1 ($string) cannot be empty
    $string3 = str_increment($empty);
    var_dump($string3);
} catch (ValueError $e) {
    echo 'str_increment() does not allow empty input.' . PHP_EOL;
}

Și un strop de sexy-balamuc tipic PHP, prin prisma căruia str_increment, respectiv str_decrement sunt totuși o idee bună. Problema e că sunt o idee bună cu mult mai mult de două caractere și, așa cum știu toți, majoritatea programatorilor sunt grabnici să scrie doar pe net.

//Inconveniently enough, 
//	this converts to int 0 AND then performs the addition, 
//	so the result is 1 rather than '1'
$zero = '0';
$string4 = ++$zero;
var_dump($string4);

$zero = '0';
//This does the job, though
$string4 = str_increment($zero);
var_dump($string4);

Ironic, pentru a obține rezultatul dorit cu operatorii de incrementare, în ăst exemplu trebuie să aplici o ilegală, adică să pleci de la un șir gol de caractere. Gol de suflet, gol de morală, gol de conținut, util fără doar și poate.

La fel ca Cel Mai Bun Ministru, funcțiile get_class() și get_parent_class() nu mai tolerează lenea

Adică apelate fără parametri vor genera avertizări de tip PHP Deprecated atunci când sunt folosite în granițele unei clase. Dacă get_class() este rulată fără parametri în afara unei clase va declanșa o eroare fatală (la fel de evitat ca o așa-zisă femme-fatale).

<?php
error_reporting(E_ALL);

class A {
    public function doStuff() {
        echo 'In A:' . PHP_EOL;
        //PHP Deprecated:  Calling get_class() without arguments 
        //	is deprecated in get_class.php on line 9
        var_dump(get_class());
    }
}

class B extends A {
    public function doStuff() {
        echo 'In B:' . PHP_EOL;
        //PHP Deprecated:  Calling get_parent_class() without arguments 
        //	is deprecated in get_class.php on line 18
        var_dump(get_parent_class());
    }
}

$a = new A();
$a->doStuff();

$b = new B();
$b->doStuff();

Presupun că la un moment dat va fi eliminat complet acest comportament oarecum bizar, ceea ce, în spiritul exprimării explicite, n-ar fi nici pe jumătate rău. În definitiv, așa cum o știm cu toții, programatorii n-au nevoie de ajutor suplimentar pentru a scrie cod haios.

Nu neapărat C#, dar ceva să placă tuturor

Ce mi-a plăcut în general las PHP și C# a fost că mereu au fost ghidate de satisfacerea unor necesități practice și nu academice, n-au țintit musai să execute sarcinile în stil Conform-cu-Presură ca și cum ar exista un premiu în puncte politice pentru așa ceva. Street-smart, cum se zice. Bagaboante.

Iată că-n PHP 8.4, păstrând spiritul demoniac ce le animă pe-amândouă, putem acum defini proprietăți de-o manieră similară cu C# (get/set). Și, surprinzător, registratura necesară nu este cu mult mai stufoasă în PHP. Iată un exemplu:

<?php
class Company {
    private $_fiscalAttribute;
    
    private $_fiscalNumber;
    
    public string $fiscalCode {
        get => sprintf('%s%s', 
            $this->_fiscalAttribute, 
            $this->_fiscalNumber);
            
        set (string $value) {
            $this->_updateFiscalProperties($value);
        }
    }
    
    private function _updateFiscalProperties($value) {
        $this->_fiscalAttribute = '';
        $this->_fiscalNumber = '';
        
        $value = trim($value !== null ? $value : '');
        if (empty($value)) {
            return;
        }
        
        $value = strtoupper($value);
        if (preg_match('/^([A-Z]{2})/i', $value)) {
            $this->_fiscalAttribute = substr($value, 0, 2);
            $this->_fiscalNumber = substr($value, 2);
        } else {
            $this->_fiscalNumber = $value;
        }
    }
    
    public function getFiscalAttribute(): string {
        return $this->_fiscalAttribute;
    }
    
    public function getFiscalNumber(): string {
        return $this->_fiscalNumber;
    }
}

$c = new Company();
$c->fiscalCode = 'RO12345678';

var_dump($c->fiscalCode);
var_dump($c->getFiscalAttribute());
var_dump($c->getFiscalNumber());

echo '============' . PHP_EOL;

$c->fiscalCode = '12345678';
var_dump($c->fiscalCode);
var_dump($c->getFiscalAttribute());
var_dump($c->getFiscalNumber());

Mai sus este o modalitate de-a defalca automat CIF-ul în atributul fiscal (ce poate să apară ca prefix) și codul propriu-zis. Uneori defalcarea este utilă, deci nu-i musai un exemplu tras de păr. Intern este stocat separat, putând fi citite fie părțile componente, fie întregul CIF.

Se observă cum este nevoie un pic mai multă muncă în scrierea procedurii set, întrucât nu este declarată automat o variabilă pentru valoarea transmisă la scriere (value în C#). Personal, nu consider că este un impediment, având în vedere că apreciez foarte mult acest stil de-a compune proprietățile.