Introducere în programarea orientată pe obiecte

Termenul “OOP” se referă la programarea orientată pe obiecte, care este o modalitate de a organiza și structura codul. OOP ne permite să vedem programul ca pe un set de obiecte care comunică între ele, în loc de o secvență de comenzi și funcții.

În OOP, un “obiect” este o unitate care conține date și funcții care lucrează cu aceste date. Obiectele sunt create conform “claselor”, pe care le putem înțelege ca planuri sau șabloane pentru obiecte. Când avem o clasă, putem crea o “instanță” a acesteia, care este un obiect specific creat conform acelei clase.

Să vedem cum putem crea o clasă simplă în PHP. Când definim o clasă, folosim cuvântul cheie “class”, urmat de numele clasei și apoi acolade care înconjoară funcțiile (numite “metode”) și variabilele clasei (numite “proprietăți”):

class Masina
{
	function claxoneaza()
	{
		echo 'Bip bip!';
	}
}

În acest exemplu, am creat o clasă numită Masina cu o singură funcție (sau “metodă”) numită claxoneaza.

Fiecare clasă ar trebui să rezolve doar o singură sarcină principală. Dacă o clasă face prea multe lucruri, ar putea fi potrivit să o împărțim în clase mai mici, specializate.

Clasele sunt de obicei stocate în fișiere separate pentru a menține codul organizat și ușor de navigat. Numele fișierului ar trebui să corespundă numelui clasei, deci pentru clasa Masina, numele fișierului ar fi Masina.php.

Când denumim clasele, este bine să respectăm convenția “PascalCase”, ceea ce înseamnă că fiecare cuvânt din nume începe cu literă mare și nu există caractere de subliniere sau alți separatori între ele. Metodele și proprietățile folosesc convenția “camelCase”, ceea ce înseamnă că încep cu literă mică.

Unele metode în PHP au roluri speciale și sunt marcate cu prefixul __ (două caractere de subliniere). Una dintre cele mai importante metode speciale este “constructorul”, care este marcat ca __construct. Constructorul este o metodă care este apelată automat atunci când creați o nouă instanță a clasei.

Constructorul este adesea folosit pentru a seta starea inițială a obiectului. De exemplu, atunci când creați un obiect care reprezintă o persoană, puteți folosi constructorul pentru a seta vârsta, numele sau alte proprietăți ale acesteia.

Să vedem cum să folosim un constructor în PHP:

class Persoana
{
	private $varsta;

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

	function catiAniAi()
	{
		return $this->varsta;
	}
}

$persoana = new Persoana(25);
echo $persoana->catiAniAi(); // Afișează: 25

În acest exemplu, clasa Persoana are o proprietate (variabilă) $varsta și un constructor care setează această proprietate. Metoda catiAniAi() permite apoi accesul la vârsta persoanei.

Pseudovariabila $this este utilizată în interiorul clasei pentru a accesa proprietățile și metodele obiectului.

Cuvântul cheie new este folosit pentru a crea o nouă instanță a clasei. În exemplul de mai sus, am creat o nouă persoană cu vârsta de 25 de ani.

Puteți, de asemenea, să setați valori implicite pentru parametrii constructorului, dacă aceștia nu sunt specificați la crearea obiectului. De exemplu:

class Persoana
{
	private $varsta;

	function __construct($varsta = 20)
	{
		$this->varsta = $varsta;
	}

	function catiAniAi()
	{
		return $this->varsta;
	}
}

$persoana = new Persoana;  // dacă nu se transmite niciun argument, parantezele pot fi omise
echo $persoana->catiAniAi(); // Afișează: 20

În acest exemplu, dacă nu specificați vârsta la crearea obiectului Persoana, va fi utilizată valoarea implicită 20.

Este convenabil că definiția proprietății cu inițializarea sa prin constructor poate fi scurtată și simplificată astfel:

class Persoana
{
	function __construct(
		private $varsta = 20,
	) {
	}
}

Pentru completitudine, pe lângă constructori, obiectele pot avea și destructori (metoda __destruct), care sunt apelați înainte ca obiectul să fie eliberat din memorie.

Spații de nume

Spațiile de nume (sau “namespaces” în engleză) ne permit să organizăm și să grupăm clase, funcții și constante înrudite, evitând în același timp conflictele de nume. Le puteți imagina ca pe niște foldere pe computer, unde fiecare folder conține fișiere care aparțin unui anumit proiect sau subiect.

Spațiile de nume sunt deosebit de utile în proiecte mai mari sau atunci când utilizați biblioteci de la terți, unde ar putea apărea conflicte în numele claselor.

Imaginați-vă că aveți o clasă numită Masina în proiectul dvs. și doriți să o plasați într-un spațiu de nume numit Transport. Faceți acest lucru astfel:

namespace Transport;

class Masina
{
	function claxoneaza()
	{
		echo 'Bip bip!';
	}
}

Dacă doriți să utilizați clasa Masina într-un alt fișier, trebuie să specificați din ce spațiu de nume provine clasa:

$masina = new Transport\Masina;

Pentru simplificare, puteți specifica la începutul fișierului ce clasă dintr-un anumit spațiu de nume doriți să utilizați, ceea ce permite crearea instanțelor fără a fi nevoie să specificați calea completă:

use Transport\Masina;

$masina = new Masina;

Moștenire

Moștenirea este un instrument al programării orientate pe obiecte care permite crearea de noi clase pe baza claselor existente, preluând proprietățile și metodele acestora și extinzându-le sau redefinindu-le după necesități. Moștenirea permite asigurarea reutilizabilității codului și a ierarhiei claselor.

Pe scurt, dacă avem o clasă și am dori să creăm alta, derivată din ea, dar cu câteva modificări, putem “moșteni” noua clasă din clasa originală.

În PHP, moștenirea se realizează folosind cuvântul cheie extends.

Clasa noastră Persoana stochează informații despre vârstă. Putem avea o altă clasă Student, care extinde Persoana și adaugă informații despre domeniul de studiu.

Să vedem un exemplu:

class Persoana
{
	private $varsta;

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

	function afiseazaInformatii()
	{
		echo "Vârstă: {$this->varsta} ani\n";
	}
}

class Student extends Persoana
{
	private $specializare;

	function __construct($varsta, $specializare)
	{
		parent::__construct($varsta);
		$this->specializare = $specializare;
	}

	function afiseazaInformatii()
	{
		parent::afiseazaInformatii();
		echo "Specializare: {$this->specializare} \n";
	}
}

$student = new Student(20, 'Informatică');
$student->afiseazaInformatii();

Cum funcționează acest cod?

  • Am folosit cuvântul cheie extends pentru a extinde clasa Persoana, ceea ce înseamnă că clasa Student moștenește toate metodele și proprietățile din Persoana.
  • Cuvântul cheie parent:: ne permite să apelăm metode din clasa părinte. În acest caz, am apelat constructorul din clasa Persoana înainte de a adăuga funcționalitatea proprie în clasa Student. Și în mod similar, metoda afiseazaInformatii() a părintelui înainte de a afișa informațiile despre student.

Moștenirea este destinată situațiilor în care există o relație “este” între clase. De exemplu, Student este o Persoana. Pisica este un animal. Ne oferă posibilitatea, în cazurile în care în cod ne așteptăm la un obiect (de ex. “Persoana”), să folosim în locul lui un obiect moștenit (de ex. “Student”).

Este important de reținut că scopul principal al moștenirii nu este evitarea duplicării codului. Dimpotrivă, utilizarea incorectă a moștenirii poate duce la un cod complex și greu de întreținut. Dacă relația “este” între clase nu există, ar trebui să luăm în considerare compoziția în locul moștenirii.

Observați că metodele afiseazaInformatii() din clasele Persoana și Student afișează informații ușor diferite. Și putem adăuga alte clase (de exemplu, Angajat), care vor oferi alte implementări ale acestei metode. Capacitatea obiectelor de diferite clase de a răspunde la aceeași metodă în moduri diferite se numește polimorfism:

$persoane = [
	new Persoana(30),
	new Student(20, 'Informatică'),
	new Angajat(45, 'Director'),
];

foreach ($persoane as $persoana) {
	$persoana->afiseazaInformatii();
}

Compoziție

Compoziția este o tehnică în care, în loc să moștenim proprietățile și metodele unei alte clase, pur și simplu folosim instanța acesteia în clasa noastră. Acest lucru ne permite să combinăm funcționalitățile și proprietățile mai multor clase fără a fi nevoie să creăm structuri de moștenire complexe.

Să vedem un exemplu. Avem clasa Motor și clasa Masina. În loc să spunem “Masina este un Motor”, spunem “Masina are un Motor”, ceea ce este o relație tipică de compoziție.

class Motor
{
	function porneste()
	{
		echo 'Motorul funcționează.';
	}
}

class Masina
{
	private $motor;

	function __construct()
	{
		$this->motor = new Motor;
	}

	function pornesteMasina()
	{
		$this->motor->porneste();
		echo 'Mașina este gata de drum!';
	}
}

$masina = new Masina;
$masina->pornesteMasina();

Aici, Masina nu are toate proprietățile și metodele lui Motor, dar are acces la acesta prin intermediul proprietății $motor.

Avantajul compoziției este flexibilitatea mai mare în design și posibilitatea mai bună de modificare în viitor.

Vizibilitate

În PHP, puteți defini “vizibilitatea” pentru proprietățile, metodele și constantele unei clase. Vizibilitatea determină de unde puteți accesa aceste elemente.

  1. Public: Dacă un element este marcat ca public, înseamnă că îl puteți accesa de oriunde, chiar și din afara clasei.
  2. Protected: Un element marcat ca protected este accesibil numai în cadrul clasei respective și al tuturor descendenților săi (clasele care moștenesc de la această clasă).
  3. Private: Dacă un element este private, îl puteți accesa numai din interiorul clasei în care a fost definit.

Dacă nu specificați vizibilitatea, PHP o va seta automat la public.

Să vedem un exemplu de cod:

class ExempluVizibilitate
{
	public $proprietatePublica = 'Publică';
	protected $proprietateProtejata = 'Protejată';
	private $proprietatePrivata = 'Privată';

	public function afiseazaProprietati()
	{
		echo $this->proprietatePublica;  // Funcționează
		echo $this->proprietateProtejata; // Funcționează
		echo $this->proprietatePrivata; // Funcționează
	}
}

$obiect = new ExempluVizibilitate;
$obiect->afiseazaProprietati();
echo $obiect->proprietatePublica;      // Funcționează
// echo $obiect->proprietateProtejata;  // Generează eroare
// echo $obiect->proprietatePrivata;  // Generează eroare

Continuăm cu moștenirea clasei:

class ClasaDescendent extends ExempluVizibilitate
{
	public function afiseazaProprietati()
	{
		echo $this->proprietatePublica;   // Funcționează
		echo $this->proprietateProtejata;  // Funcționează
		// echo $this->proprietatePrivata;  // Generează eroare
	}
}

În acest caz, metoda afiseazaProprietati() din clasa ClasaDescendent poate accesa proprietățile publice și protejate, dar nu poate accesa proprietățile private ale clasei părinte.

Datele și metodele ar trebui să fie cât mai ascunse posibil și accesibile numai printr-o interfață definită. Acest lucru vă permite să modificați implementarea internă a clasei fără a afecta restul codului.

Cuvântul cheie final

În PHP, putem folosi cuvântul cheie final dacă dorim să împiedicăm o clasă, metodă sau constantă să fie moștenită sau suprascrisă. Când marcăm o clasă ca final, aceasta nu poate fi extinsă. Când marcăm o metodă ca final, aceasta nu poate fi suprascrisă într-o clasă descendentă.

Conștientizarea faptului că o anumită clasă sau metodă nu va fi modificată ulterior ne permite să facem modificări mai ușor, fără a ne face griji cu privire la posibile conflicte. De exemplu, putem adăuga o nouă metodă fără teama că un descendent al său ar avea deja o metodă cu același nume și ar apărea o coliziune. Sau putem modifica parametrii unei metode, deoarece din nou nu există riscul de a provoca o neconcordanță cu metoda suprascrisă într-un descendent.

final class ClasaFinala
{
}

// Următorul cod va genera o eroare, deoarece nu putem moșteni de la o clasă finală.
class DescendentClasaFinala extends ClasaFinala
{
}

În acest exemplu, încercarea de a moșteni de la clasa finală ClasaFinala va genera o eroare.

Proprietăți și metode statice

Când vorbim în PHP despre elemente “statice” ale unei clase, ne referim la metode și proprietăți care aparțin clasei în sine, și nu unei instanțe specifice a acestei clase. Acest lucru înseamnă că nu trebuie să creați o instanță a clasei pentru a avea acces la ele. În schimb, le apelați sau accesați direct prin numele clasei.

Rețineți că, deoarece elementele statice aparțin clasei și nu instanțelor sale, nu puteți utiliza pseudovariabila $this în interiorul metodelor statice.

Utilizarea proprietăților statice duce la cod neclar plin de capcane, de aceea nu ar trebui să le folosiți niciodată și nici nu vom arăta aici un exemplu de utilizare. În schimb, metodele statice sunt utile. Exemplu de utilizare:

class Calculator
{
	public static function adunare($a, $b)
	{
		return $a + $b;
	}

	public static function scadere($a, $b)
	{
		return $a - $b;
	}
}

// Utilizarea metodei statice fără a crea o instanță a clasei
echo Calculator::adunare(5, 3); // Rezultat: 8
echo Calculator::scadere(5, 3); // Rezultat: 2

În acest exemplu, am creat clasa Calculator cu două metode statice. Putem apela aceste metode direct fără a crea o instanță a clasei folosind operatorul ::. Metodele statice sunt deosebit de utile pentru operații care nu depind de starea unei instanțe specifice a clasei.

Constante de clasă

În cadrul claselor, avem posibilitatea de a defini constante. Constantele sunt valori care nu se schimbă niciodată în timpul execuției programului. Spre deosebire de variabile, valoarea unei constante rămâne mereu aceeași.

class Masina
{
	public const NumarRoti = 4;

	public function afiseazaNumarRoti(): int
	{
		echo self::NumarRoti;
	}
}

echo Masina::NumarRoti;  // Ieșire: 4

În acest exemplu, avem clasa Masina cu constanta NumarRoti. Când dorim să accesăm constanta în interiorul clasei, putem folosi cuvântul cheie self în locul numelui clasei.

Interfețe de obiecte

Interfețele de obiecte funcționează ca niște “contracte” pentru clase. Dacă o clasă trebuie să implementeze o interfață de obiect, trebuie să conțină toate metodele pe care le definește acea interfață. Este o modalitate excelentă de a asigura că anumite clase respectă același “contract” sau structură.

În PHP, interfețele se definesc cu cuvântul cheie interface. Toate metodele definite într-o interfață sunt publice (public). Când o clasă implementează o interfață, folosește cuvântul cheie implements.

interface Animal
{
	function scoateSunet();
}

class Pisica implements Animal
{
	public function scoateSunet()
	{
		echo 'Miau';
	}
}

$pisica = new Pisica;
$pisica->scoateSunet();

Dacă o clasă implementează o interfață, dar nu sunt definite în ea toate metodele așteptate, PHP va genera o eroare.

O clasă poate implementa mai multe interfețe simultan, ceea ce este o diferență față de moștenire, unde o clasă poate moșteni doar de la o singură clasă:

interface Paznic
{
	function pazesteCasa();
}

class Caine implements Animal, Paznic
{
	public function scoateSunet()
	{
		echo 'Ham';
	}

	public function pazesteCasa()
	{
		echo 'Câinele păzește cu atenție casa';
	}
}

Clase abstracte

Clasele abstracte servesc ca șabloane de bază pentru alte clase, dar nu puteți crea instanțe ale acestora direct. Ele conțin o combinație de metode complete și metode abstracte, care nu au conținut definit. Clasele care moștenesc de la clase abstracte trebuie să furnizeze definiții pentru toate metodele abstracte ale părintelui.

Pentru a defini o clasă abstractă, folosim cuvântul cheie abstract.

abstract class ClasaAbstracta
{
	public function metodaObisnuita()
	{
		echo 'Aceasta este o metodă obișnuită';
	}

	abstract public function metodaAbstracta();
}

class Descendent extends ClasaAbstracta
{
	public function metodaAbstracta()
	{
		echo 'Aceasta este implementarea metodei abstracte';
	}
}

$instanta = new Descendent;
$instanta->metodaObisnuita();
$instanta->metodaAbstracta();

În acest exemplu, avem o clasă abstractă cu o metodă obișnuită și una abstractă. Apoi avem clasa Descendent, care moștenește de la ClasaAbstracta și furnizează implementarea pentru metoda abstractă.

Cum diferă de fapt interfețele și clasele abstracte? Clasele abstracte pot conține atât metode abstracte, cât și concrete, în timp ce interfețele definesc doar ce metode trebuie să implementeze o clasă, dar nu oferă nicio implementare. O clasă poate moșteni doar de la o singură clasă abstractă, dar poate implementa un număr nelimitat de interfețe.

Verificarea tipurilor

În programare, este foarte important să fim siguri că datele cu care lucrăm sunt de tipul corect. În PHP, avem instrumente care ne asigură acest lucru. Verificarea dacă datele au tipul corect se numește “verificarea tipurilor”.

Tipurile pe care le putem întâlni în PHP:

  1. Tipuri de bază: Includ int (numere întregi), float (numere zecimale), bool (valori de adevăr), string (șiruri de caractere), array (tablouri) și null.
  2. Clase: Dacă dorim ca valoarea să fie o instanță a unei clase specifice.
  3. Interfețe: Definește un set de metode pe care o clasă trebuie să le implementeze. O valoare care respectă interfața trebuie să aibă aceste metode.
  4. Tipuri mixte: Putem specifica că o variabilă poate avea mai multe tipuri permise.
  5. Void: Acest tip special indică faptul că o funcție sau metodă nu returnează nicio valoare.

Să vedem cum să modificăm codul pentru a include tipuri:

class Persoana
{
	private int $varsta;

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

	public function afiseazaVarsta(): void
	{
		echo "Această persoană are {$this->varsta} ani.";
	}
}

/**
 * Funcție care primește un obiect al clasei Persoana și afișează vârsta persoanei.
 */
function afiseazaVarstaPersoanei(Persoana $persoana): void
{
	$persoana->afiseazaVarsta();
}

În acest fel, ne-am asigurat că codul nostru așteaptă și lucrează cu date de tipul corect, ceea ce ne ajută să prevenim potențiale erori.

Unele tipuri nu pot fi scrise direct în PHP. În acest caz, ele sunt specificate într-un comentariu phpDoc, care este un format standard pentru documentarea codului PHP, începând cu /** și terminând cu */. Permite adăugarea de descrieri pentru clase, metode etc. Și, de asemenea, specificarea tipurilor complexe folosind așa-numitele adnotări @var, @param și @return. Aceste tipuri sunt apoi utilizate de instrumentele de analiză statică a codului, dar PHP în sine nu le verifică.

class Lista
{
	/** @var array<Persoana> notația indică faptul că este un array de obiecte Persoana */
	private array $persoane = [];

	public function adaugaPersoana(Persoana $persoana): void
	{
		$this->persoane[] = $persoana;
	}
}

Comparație și identitate

În PHP, puteți compara obiecte în două moduri:

  1. Comparația valorilor ==: Verifică dacă obiectele sunt de aceeași clasă și au aceleași valori în proprietățile lor.
  2. Identitatea ===: Verifică dacă este aceeași instanță a obiectului.
class Masina
{
	public string $marca;

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

$masina1 = new Masina('Skoda');
$masina2 = new Masina('Skoda');
$masina3 = $masina1;

var_dump($masina1 == $masina2);   // true, deoarece au aceeași valoare
var_dump($masina1 === $masina2);  // false, deoarece nu sunt aceeași instanță
var_dump($masina1 === $masina3);  // true, deoarece $masina3 este aceeași instanță ca $masina1

Operatorul instanceof

Operatorul instanceof permite verificarea dacă un obiect dat este o instanță a unei anumite clase, un descendent al acestei clase sau dacă implementează o anumită interfață.

Să ne imaginăm că avem clasa Persoana și o altă clasă Student, care este un descendent al clasei Persoana:

class Persoana
{
	private int $varsta;

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

class Student extends Persoana
{
	private string $specializare;

	public function __construct(int $varsta, string $specializare)
	{
		parent::__construct($varsta);
		$this->specializare = $specializare;
	}
}

$student = new Student(20, 'Informatică');

// Verifică dacă $student este o instanță a clasei Student
var_dump($student instanceof Student);  // Ieșire: bool(true)

// Verifică dacă $student este o instanță a clasei Persoana (deoarece Student este descendent al Persoana)
var_dump($student instanceof Persoana);     // Ieșire: bool(true)

Din ieșiri reiese că obiectul $student este considerat simultan o instanță a ambelor clase – Student și Persoana.

Interfețe fluente

“Interfața fluentă” (în engleză “Fluent Interface”) este o tehnică în OOP care permite înlănțuirea metodelor într-un singur apel. Acest lucru simplifică adesea și clarifică codul.

Elementul cheie al unei interfețe fluente este că fiecare metodă din lanț returnează o referință la obiectul curent. Realizăm acest lucru folosind return $this; la sfârșitul metodei. Acest stil de programare este adesea asociat cu metodele numite “setters”, care setează valorile proprietăților obiectului.

Să vedem cum poate arăta o interfață fluentă pe exemplul trimiterii de emailuri:

public function trimiteMesaj()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

În acest exemplu, metodele setFrom(), setRecipient() și setMessage() servesc la setarea valorilor corespunzătoare (expeditor, destinatar, conținutul mesajului). După setarea fiecăreia dintre aceste valori, metodele ne returnează obiectul curent ($email), ceea ce ne permite să înlănțuim următoarea metodă după ea. În final, apelăm metoda send(), care trimite efectiv emailul.

Datorită interfețelor fluente, putem scrie cod care este intuitiv și ușor de citit.

Copierea folosind clone

În PHP, putem crea o copie a unui obiect folosind operatorul clone. În acest fel, obținem o nouă instanță cu conținut identic.

Dacă trebuie să modificăm unele proprietăți ale obiectului la copiere, putem defini în clasă o metodă specială __clone(). Această metodă este apelată automat atunci când obiectul este clonat.

class Oaie
{
	public string $nume;

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

	public function __clone()
	{
		$this->nume = 'Clona ' . $this->nume;
	}
}

$original = new Oaie('Dolly');
echo $original->nume . "\n";  // Afișează: Dolly

$clona = clone $original;
echo $clona->nume . "\n";      // Afișează: Clona Dolly

În acest exemplu, avem clasa Oaie cu o singură proprietate $nume. Când clonăm o instanță a acestei clase, metoda __clone() se asigură că numele oii clonate primește prefixul “Clona”.

Trait-uri

Trait-urile în PHP sunt un instrument care permite partajarea metodelor, proprietăților și constantelor între clase și evitarea duplicării codului. Le puteți imagina ca pe un mecanism de “copiere și lipire” (Ctrl-C și Ctrl-V), în care conținutul trait-ului este “inserat” în clase. Acest lucru vă permite să reutilizați codul fără a fi nevoie să creați ierarhii de clase complicate.

Să vedem un exemplu simplu despre cum să folosim trait-uri în PHP:

trait Claxonare
{
	public function claxoneaza()
	{
		echo 'Bip bip!';
	}
}

class Masina
{
	use Claxonare;
}

class Camion
{
	use Claxonare;
}

$masina = new Masina;
$masina->claxoneaza(); // Afișează 'Bip bip!'

$camion = new Camion;
$camion->claxoneaza(); // De asemenea, afișează 'Bip bip!'

În acest exemplu, avem un trait numit Claxonare, care conține o singură metodă claxoneaza(). Apoi avem două clase: Masina și Camion, care ambele folosesc trait-ul Claxonare. Datorită acestui fapt, ambele clase “au” metoda claxoneaza(), și o putem apela pe obiectele ambelor clase.

Trait-urile vă permit să partajați codul între clase ușor și eficient. În același timp, ele nu intră în ierarhia de moștenire, adică $masina instanceof Claxonare va returna false.

Excepții

Excepțiile în OOP ne permit să gestionăm elegant erorile și situațiile neașteptate în codul nostru. Sunt obiecte care conțin informații despre eroare sau situația neobișnuită.

În PHP, avem clasa încorporată Exception, care servește ca bază pentru toate excepțiile. Aceasta are mai multe metode care ne permit să obținem mai multe informații despre excepție, cum ar fi mesajul de eroare, fișierul și linia unde a apărut eroarea etc.

Când apare o eroare în cod, putem “arunca” o excepție folosind cuvântul cheie throw.

function impartire(float $a, float $b): float
{
	if ($b === 0) {
		throw new Exception('Împărțire la zero!');
	}
	return $a / $b;
}

Când funcția impartire() primește zero ca al doilea argument, aruncă o excepție cu mesajul de eroare 'Împărțire la zero!'. Pentru a preveni căderea programului la aruncarea unei excepții, o prindem într-un bloc try/catch:

try {
	echo impartire(10, 0);
} catch (Exception $e) {
	echo 'Excepție prinsă: '. $e->getMessage();
}

Codul care poate arunca o excepție este încapsulat într-un bloc try. Dacă o excepție este aruncată, execuția codului se mută în blocul catch, unde putem procesa excepția (de exemplu, afișând un mesaj de eroare).

După blocurile try și catch, putem adăuga un bloc opțional finally, care se execută întotdeauna, indiferent dacă a fost aruncată sau nu o excepție (chiar și în cazul în care în blocul try sau catch folosim instrucțiunea return, break sau continue):

try {
	echo impartire(10, 0);
} catch (Exception $e) {
	echo 'Excepție prinsă: '. $e->getMessage();
} finally {
	// Codul care se execută întotdeauna, indiferent dacă a fost aruncată sau nu o excepție
}

Putem, de asemenea, crea propriile clase (ierarhie) de excepții, care moștenesc de la clasa Exception. Ca exemplu, să ne imaginăm o aplicație bancară simplă care permite efectuarea de depuneri și retrageri:

class ExceptieBancara extends Exception {}
class ExceptieFonduriInsuficiente extends ExceptieBancara {}
class ExceptieLimitaDepasita extends ExceptieBancara {}

class ContBancar
{
	private int $sold = 0;
	private int $limitaZilnica = 1000;

	public function depune(int $suma): int
	{
		$this->sold += $suma;
		return $this->sold;
	}

	public function retrage(int $suma): int
	{
		if ($suma > $this->sold) {
			throw new ExceptieFonduriInsuficiente('Nu există suficiente fonduri în cont.');
		}

		if ($suma > $this->limitaZilnica) {
			throw new ExceptieLimitaDepasita('Limita zilnică pentru retrageri a fost depășită.');
		}

		$this->sold -= $suma;
		return $this->sold;
	}
}

Pentru un singur bloc try, se pot specifica mai multe blocuri catch, dacă vă așteptați la diferite tipuri de excepții.

$cont = new ContBancar;
$cont->depune(500);

try {
	$cont->retrage(1500);
} catch (ExceptieLimitaDepasita $e) {
	echo $e->getMessage();
} catch (ExceptieFonduriInsuficiente $e) {
	echo $e->getMessage();
} catch (ExceptieBancara $e) {
	echo 'A apărut o eroare în timpul efectuării operațiunii.';
}

În acest exemplu, este important de observat ordinea blocurilor catch. Deoarece toate excepțiile moștenesc de la ExceptieBancara, dacă am avea acest bloc primul, toate excepțiile ar fi prinse în el, fără ca codul să ajungă la blocurile catch următoare. De aceea, este important să avem excepțiile mai specifice (adică cele care moștenesc de la altele) în blocul catch mai sus în ordine decât excepțiile lor părinte.

Iterație

În PHP, puteți parcurge obiecte folosind bucla foreach, similar cu parcurgerea array-urilor. Pentru ca acest lucru să funcționeze, obiectul trebuie să implementeze o interfață specială.

Prima opțiune este implementarea interfeței Iterator, care are metodele current() returnând valoarea curentă, key() returnând cheia, next() trecând la următoarea valoare, rewind() revenind la început și valid() verificând dacă nu am ajuns încă la sfârșit.

A doua opțiune este implementarea interfeței IteratorAggregate, care are doar o singură metodă getIterator(). Aceasta returnează fie un obiect substitut care va asigura parcurgerea, fie poate reprezenta un generator, care este o funcție specială în care se folosește yield pentru a returna succesiv chei și valori:

class Persoana
{
	public function __construct(
		public int $varsta,
	) {
	}
}

class Lista implements IteratorAggregate
{
	private array $persoane = [];

	public function adaugaPersoana(Persoana $persoana): void
	{
		$this->persoane[] = $persoana;
	}

	public function getIterator(): Generator
	{
		foreach ($this->persoane as $persoana) {
			yield $persoana;
		}
	}
}

$lista = new Lista;
$lista->adaugaPersoana(new Persoana(30));
$lista->adaugaPersoana(new Persoana(25));

foreach ($lista as $persoana) {
	echo "Vârstă: {$persoana->varsta} ani \n";
}

Bune practici

După ce ați însușit principiile de bază ale programării orientate pe obiecte, este important să vă concentrați asupra bunelor practici în OOP. Acestea vă vor ajuta să scrieți cod care nu este doar funcțional, ci și lizibil, ușor de înțeles și ușor de întreținut.

  1. Separarea responsabilităților (Separation of Concerns): Fiecare clasă ar trebui să aibă o responsabilitate clar definită și ar trebui să rezolve doar o singură sarcină principală. Dacă o clasă face prea multe lucruri, poate fi potrivit să o împărțiți în clase mai mici, specializate.
  2. Încapsularea (Encapsulation): Datele și metodele ar trebui să fie cât mai ascunse posibil și accesibile numai printr-o interfață definită. Acest lucru vă permite să modificați implementarea internă a clasei fără a afecta restul codului.
  3. Injectarea dependențelor (Dependency Injection): În loc să creați dependențe direct în clasă, ar trebui să le “injectați” din exterior. Pentru o înțelegere mai profundă a acestui principiu, recomandăm capitolele despre Dependency Injection.
versiune: 4.0