Uvod v objektno usmerjeno programiranje

Izraz “OOP” označuje objektno usmerjeno programiranje, kar je način organiziranja in strukturiranja kode. OOP nam omogoča, da program vidimo kot zbirko objektov, ki med seboj komunicirajo, namesto zaporedja ukazov in funkcij.

V OOP je “objekt” enota, ki vsebuje podatke in funkcije, ki delujejo s temi podatki. Objekti so ustvarjeni po “razredih”, ki jih lahko razumemo kot načrte ali predloge za objekte. Ko imamo razred, lahko ustvarimo njegovo “instanco”, kar je konkreten objekt, ustvarjen po tem razredu.

Poglejmo si, kako lahko ustvarimo preprost razred v PHP. Pri definiranju razreda uporabimo ključno besedo “class”, ki ji sledi ime razreda, nato pa zaviti oklepaji, ki obdajajo funkcije (imenovane “metode”) in spremenljivke razreda (imenovane “lastnosti” ali angleško “property”):

class Avto
{
	function potrobi()
	{
		echo 'Bip bip!';
	}
}

V tem primeru smo ustvarili razred z imenom Avto z eno funkcijo (ali “metodo”), imenovano potrobi.

Vsak razred bi moral reševati samo eno glavno nalogo. Če razred počne preveč stvari, ga je morda primerno razdeliti na manjše, specializirane razrede.

Razrede običajno shranjujemo v ločene datoteke, da je koda organizirana in se v njej lahko enostavno orientiramo. Ime datoteke bi moralo ustrezati imenu razreda, tako da bi za razred Avto ime datoteke bilo Avto.php.

Pri poimenovanju razredov je dobro slediti konvenciji “PascalCase”, kar pomeni, da se vsaka beseda v imenu začne z veliko začetnico in med njimi ni podčrtajev ali drugih ločil. Metode in lastnosti uporabljajo konvencijo “camelCase”, kar pomeni, da se začnejo z malo začetnico.

Nekatere metode v PHP imajo posebne naloge in so označene s predpono __ (dva podčrtaja). Ena najpomembnejših posebnih metod je “konstruktor”, ki je označen kot __construct. Konstruktor je metoda, ki se samodejno pokliče, ko ustvarite novo instanco razreda.

Konstruktor pogosto uporabljamo za nastavitev začetnega stanja objekta. Na primer, ko ustvarjate objekt, ki predstavlja osebo, lahko uporabite konstruktor za nastavitev njene starosti, imena ali drugih lastnosti.

Poglejmo si, kako uporabiti konstruktor v PHP:

class Oseba
{
	private $starost;

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

	function kolikoSiStar()
	{
		return $this->starost;
	}
}

$oseba = new Oseba(25);
echo $oseba->kolikoSiStar(); // Izpiše: 25

V tem primeru ima razred Oseba lastnost (spremenljivko) $starost in konstruktor, ki nastavi to lastnost. Metoda kolikoSiStar() nato omogoča dostop do starosti osebe.

Psevdo-spremenljivka $this se uporablja znotraj razreda za dostop do lastnosti in metod objekta.

Ključna beseda new se uporablja za ustvarjanje nove instance razreda. V zgornjem primeru smo ustvarili novo osebo s starostjo 25.

Lahko nastavite tudi privzete vrednosti za parametre konstruktorja, če niso določene pri ustvarjanju objekta. Na primer:

class Oseba
{
	private $starost;

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

	function kolikoSiStar()
	{
		return $this->starost;
	}
}

$oseba = new Oseba;  // če ne posredujemo nobenega argumenta, lahko oklepaje izpustimo
echo $oseba->kolikoSiStar(); // Izpiše: 20

V tem primeru, če ne določite starosti pri ustvarjanju objekta Oseba, bo uporabljena privzeta vrednost 20.

Prijetno je, da se definicija lastnosti z njeno inicializacijo preko konstruktorja lahko tako skrajša in poenostavi:

class Oseba
{
	function __construct(
		private $starost = 20,
	) {
	}
}

Za popolnost, poleg konstruktorjev imajo lahko objekti tudi destruktorje (metoda __destruct), ki se pokličejo, preden se objekt sprosti iz pomnilnika.

Imenski prostori

Imenski prostori (ali “namespaces” v angleščini) nam omogočajo organiziranje in združevanje povezanih razredov, funkcij in konstant, hkrati pa se izogibamo konfliktom v imenih. Lahko si jih predstavljate kot mape v računalniku, kjer vsaka mapa vsebuje datoteke, ki pripadajo določenemu projektu ali temi.

Imenski prostori so še posebej uporabni pri večjih projektih ali ko uporabljate knjižnice tretjih oseb, kjer bi lahko prišlo do konfliktov v imenih razredov.

Predstavljajte si, da imate v svojem projektu razred z imenom Avto in ga želite umestiti v imenski prostor, imenovan Doprava. To storite takole:

namespace Doprava;

class Avto
{
	function potrobi()
	{
		echo 'Bip bip!';
	}
}

Če želite uporabiti razred Avto v drugi datoteki, morate določiti, iz katerega imenskega prostora razred izvira:

$avto = new Doprava\Avto;

Za poenostavitev lahko na začetku datoteke navedete, kateri razred iz danega imenskega prostora želite uporabljati, kar omogoča ustvarjanje instanc brez potrebe po navajanju celotne poti:

use Doprava\Avto;

$avto = new Avto;

Dedovanje

Dedovanje je orodje objektno usmerjenega programiranja, ki omogoča ustvarjanje novih razredov na podlagi že obstoječih razredov, prevzemanje njihovih lastnosti in metod ter njihovo razširjanje ali ponovno definiranje po potrebi. Dedovanje omogoča zagotavljanje ponovne uporabnosti kode in hierarhije razredov.

Poenostavljeno rečeno, če imamo en razred in bi želeli ustvariti drugega, iz njega izpeljanega, vendar z nekaj spremembami, lahko nov razred “podedujemo” iz prvotnega razreda.

V PHP dedovanje izvedemo s ključno besedo extends.

Naš razred Oseba hrani informacijo o starosti. Lahko imamo drug razred Student, ki razširja Osebo in dodaja informacijo o smeri študija.

Poglejmo si primer:

class Oseba
{
	private $starost;

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

	function izpisiInformacije()
	{
		echo "Starost: {$this->starost} let\n";
	}
}

class Student extends Oseba
{
	private $smer;

	function __construct($starost, $smer)
	{
		parent::__construct($starost);
		$this->smer = $smer;
	}

	function izpisiInformacije()
	{
		parent::izpisiInformacije();
		echo "Smer študija: {$this->smer} \n";
	}
}

$student = new Student(20, 'Informatika');
$student->izpisiInformacije();

Kako ta koda deluje?

  • Uporabili smo ključno besedo extends za razširitev razreda Oseba, kar pomeni, da razred Student podeduje vse metode in lastnosti iz Osebe.
  • Ključna beseda parent:: nam omogoča klicanje metod iz nadrejenega razreda. V tem primeru smo klicali konstruktor iz razreda Oseba, preden smo dodali lastno funkcionalnost v razred Student. Podobno smo klicali tudi metodo izpisiInformacije() prednika, preden smo izpisali informacije o študentu.

Dedovanje je namenjeno situacijam, ko obstaja odnos “je” med razredi. Na primer, Student je Oseba. Mačka je žival. Omogoča nam, da v primerih, ko v kodi pričakujemo en objekt (npr. “Oseba”), namesto njega uporabimo podedovani objekt (npr. “Student”).

Pomembno je vedeti, da glavni namen dedovanja ni preprečevanje podvajanja kode. Nasprotno, nepravilna uporaba dedovanja lahko vodi do zapletene in težko vzdrževane kode. Če odnos “je” med razredi ne obstaja, bi morali namesto dedovanja razmisliti o kompoziciji.

Opazite, da metodi izpisiInformacije() v razredih Oseba in Student izpisujeta nekoliko drugačne informacije. In lahko dodamo druge razrede (na primer Zaposleni), ki bodo zagotavljali druge implementacije te metode. Sposobnost objektov različnih razredov, da se na isto metodo odzovejo na različne načine, se imenuje polimorfizem:

$osebe = [
	new Oseba(30),
	new Student(20, 'Informatika'),
	new Zaposleni(45, 'Direktor'), // Predpostavimo, da razred Zaposleni obstaja
];

foreach ($osebe as $oseba) {
	$oseba->izpisiInformacije();
}

Kompozicija

Kompozicija je tehnika, pri kateri namesto da podedujemo lastnosti in metode drugega razreda, preprosto uporabimo njegovo instanco v našem razredu. To nam omogoča kombiniranje funkcionalnosti in lastnosti več razredov brez potrebe po ustvarjanju zapletenih dednih struktur.

Poglejmo si primer. Imamo razred Motor in razred Avto. Namesto da bi rekli “Avto je Motor”, rečemo “Avto ima Motor”, kar je tipičen odnos kompozicije.

class Motor
{
	function vklopi()
	{
		echo 'Motor teče.';
	}
}

class Avto
{
	private $motor;

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

	function zazeni()
	{
		$this->motor->vklopi();
		echo 'Avto je pripravljen na vožnjo!';
	}
}

$avto = new Avto;
$avto->zazeni();

Tukaj Avto nima vseh lastnosti in metod Motorja, vendar ima dostop do njega preko lastnosti $motor.

Prednost kompozicije je večja fleksibilnost pri oblikovanju in boljša možnost prilagajanja v prihodnosti.

Vidnost

V PHP lahko definirate “vidnost” za lastnosti, metode in konstante razreda. Vidnost določa, od kod lahko dostopate do teh elementov.

  1. Public: Če je element označen kot public, pomeni, da lahko do njega dostopate od kjerkoli, tudi zunaj razreda.
  2. Protected: Element z oznako protected je dostopen samo znotraj danega razreda in vseh njegovih potomcev (razredov, ki dedujejo od tega razreda).
  3. Private: Če je element private, lahko do njega dostopate samo znotraj razreda, v katerem je bil definiran.

Če ne določite vidnosti, jo PHP samodejno nastavi na public.

Poglejmo si vzorčno kodo:

class PrimerVidnosti
{
	public $javnaLastnost = 'Javna';
	protected $zascitenaLastnost = 'Zaščitena';
	private $zasebnaLastnost = 'Zasebna';

	public function izpisiLastnosti()
	{
		echo $this->javnaLastnost;  // Deluje
		echo $this->zascitenaLastnost; // Deluje
		echo $this->zasebnaLastnost; // Deluje
	}
}

$objekt = new PrimerVidnosti;
$objekt->izpisiLastnosti();
echo $objekt->javnaLastnost;      // Deluje
// echo $objekt->zascitenaLastnost;  // Javi napako
// echo $objekt->zasebnaLastnost;  // Javi napako

Nadaljujemo z dedovanjem razreda:

class PotomecRazreda extends PrimerVidnosti
{
	public function izpisiLastnosti()
	{
		echo $this->javnaLastnost;   // Deluje
		echo $this->zascitenaLastnost;  // Deluje
		// echo $this->zasebnaLastnost;  // Javi napako
	}
}

V tem primeru lahko metoda izpisiLastnosti() v razredu PotomecRazreda dostopa do javnih in zaščitenih lastnosti, ne more pa dostopati do zasebnih lastnosti starševskega razreda.

Podatki in metode bi morali biti čim bolj skriti in dostopni samo preko definiranega vmesnika. To vam omogoča spreminjanje interne implementacije razreda brez vpliva na preostalo kodo.

Ključna beseda final

V PHP lahko uporabimo ključno besedo final, če želimo preprečiti, da bi bil razred, metoda ali konstanta podedovana ali prepisana. Ko označimo razred kot final, ga ni mogoče razširiti. Ko označimo metodo kot final, je ni mogoče prepisati v potomskem razredu.

Zavedanje, da določen razred ali metoda ne bo dalje spreminjana, nam omogoča lažje izvajanje prilagoditev, ne da bi se morali bati možnih konfliktov. Na primer, lahko dodamo novo metodo brez skrbi, da bi kateri koli njen potomec že imel metodo z istim imenom in bi prišlo do trka. Ali pa lahko metodi spremenimo njene parametre, saj spet ni nevarnosti, da bi povzročili neskladje s prepisano metodo v potomcu.

final class KoncniRazred
{
}

// Naslednja koda bo javila napako, ker ne moremo dedovati od končnega razreda.
class PotomecKoncnegaRazreda extends KoncniRazred
{
}

V tem primeru bo poskus dedovanja od končnega razreda KoncniRazred javil napako.

Statične lastnosti in metode

Ko v PHP govorimo o “statičnih” elementih razreda, mislimo na metode in lastnosti, ki pripadajo samemu razredu in ne konkretni instanci tega razreda. To pomeni, da vam ni treba ustvariti instance razreda, da bi imeli dostop do njih. Namesto tega jih kličete ali dostopate do njih neposredno preko imena razreda.

Upoštevajte, da ker statični elementi pripadajo razredu in ne njegovim instancam, znotraj statičnih metod ne morete uporabljati psevdo-spremenljivke $this.

Uporaba statičnih lastnosti vodi do nepregledni kodi, polni pasti, zato jih ne bi smeli nikoli uporabiti in tukaj tudi ne bomo prikazali primera uporabe. Nasprotno pa so statične metode uporabne. Primer uporabe:

class Kalkulator
{
	public static function sestevanje($a, $b)
	{
		return $a + $b;
	}

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

// Uporaba statične metode brez ustvarjanja instance razreda
echo Kalkulator::sestevanje(5, 3); // Rezultat: 8
echo Kalkulator::odstevanje(5, 3); // Rezultat: 2

V tem primeru smo ustvarili razred Kalkulator z dvema statičnima metodama. Te metode lahko kličemo neposredno brez ustvarjanja instance razreda z uporabo operatorja ::. Statične metode so še posebej uporabne za operacije, ki niso odvisne od stanja konkretne instance razreda.

Razredne konstante

Znotraj razredov imamo možnost definirati konstante. Konstante so vrednosti, ki se nikoli ne spremenijo med izvajanjem programa. Za razliko od spremenljivk, vrednost konstante ostaja vedno enaka.

class Avto
{
	public const SteviloKoles = 4;

	public function prikaziSteviloKoles(): int
	{
		echo self::SteviloKoles;
	}
}

echo Avto::SteviloKoles;  // Izpis: 4

V tem primeru imamo razred Avto s konstanto SteviloKoles. Ko želimo dostopati do konstante znotraj razreda, lahko uporabimo ključno besedo self namesto imena razreda.

Objektni vmesniki

Objektni vmesniki delujejo kot “pogodbe” za razrede. Če mora razred implementirati objektni vmesnik, mora vsebovati vse metode, ki jih ta vmesnik definira. To je odličen način za zagotovitev, da določeni razredi upoštevajo isto “pogodbo” ali strukturo.

V PHP se vmesnik definira s ključno besedo interface. Vse metode, definirane v vmesniku, so javne (public). Ko razred implementira vmesnik, uporablja ključno besedo implements.

interface Zival
{
	function izdajZvok();
}

class Macka implements Zival
{
	public function izdajZvok()
	{
		echo 'Mijav';
	}
}

$macka = new Macka;
$macka->izdajZvok();

Če razred implementira vmesnik, vendar v njem niso definirane vse pričakovane metode, bo PHP javil napako.

Razred lahko implementira več vmesnikov hkrati, kar je razlika v primerjavi z dedovanjem, kjer lahko razred deduje samo od enega razreda:

interface Varuh
{
	function varujHiso();
}

class Pes implements Zival, Varuh
{
	public function izdajZvok()
	{
		echo 'Hov';
	}

	public function varujHiso()
	{
		echo 'Pes skrbno varuje hišo';
	}
}

Abstraktni razredi

Abstraktni razredi služijo kot osnovne predloge za druge razrede, vendar njihovih instanc ne morete ustvariti neposredno. Vsebujejo kombinacijo popolnih metod in abstraktnih metod, ki nimajo definirane vsebine. Razredi, ki dedujejo od abstraktnih razredov, morajo zagotoviti definicije za vse abstraktne metode iz prednika.

Za definiranje abstraktnega razreda uporabljamo ključno besedo abstract.

abstract class AbstraktniRazred
{
	public function navadnaMetoda()
	{
		echo 'To je navadna metoda';
	}

	abstract public function abstraktnaMetoda();
}

class Potomec extends AbstraktniRazred
{
	public function abstraktnaMetoda()
	{
		echo 'To je implementacija abstraktne metode';
	}
}

$instanca = new Potomec;
$instanca->navadnaMetoda();
$instanca->abstraktnaMetoda();

V tem primeru imamo abstraktni razred z eno navadno in eno abstraktno metodo. Nato imamo razred Potomec, ki deduje od AbstraktniRazred in zagotavlja implementacijo za abstraktno metodo.

Kako se pravzaprav razlikujejo vmesniki in abstraktni razredi? Abstraktni razredi lahko vsebujejo tako abstraktne kot konkretne metode, medtem ko vmesniki samo definirajo, katere metode mora razred implementirati, vendar ne zagotavljajo nobene implementacije. Razred lahko deduje samo od enega abstraktnega razreda, lahko pa implementira poljubno število vmesnikov.

Preverjanje tipov

V programiranju je zelo pomembno imeti gotovost, da so podatki, s katerimi delamo, pravilnega tipa. V PHP imamo orodja, ki nam to zagotavljajo. Preverjanje, ali imajo podatki pravilen tip, se imenuje “preverjanje tipov”.

Tipi, na katere lahko naletimo v PHP:

  1. Osnovni tipi: Vključujejo int (cela števila), float (decimalna števila), bool (logične vrednosti), string (nizi), array (polja) in null.
  2. Razredi: Če želimo, da je vrednost instanca specifičnega razreda.
  3. Vmesniki: Definira nabor metod, ki jih mora razred implementirati. Vrednost, ki izpolnjuje vmesnik, mora imeti te metode.
  4. Mešani tipi: Lahko določimo, da ima spremenljivka lahko več dovoljenih tipov.
  5. Void: Ta poseben tip označuje, da funkcija ali metoda ne vrača nobene vrednosti.

Poglejmo si, kako prilagoditi kodo, da bo vključevala tipe:

class Oseba
{
	private int $starost;

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

	public function izpisiStarost(): void
	{
		echo "Ta oseba je stara {$this->starost} let.";
	}
}

/**
 * Funkcija, ki sprejme objekt razreda Oseba in izpiše starost osebe.
 */
function izpisiStarostOsebe(Oseba $oseba): void
{
	$oseba->izpisiStarost();
}

Na ta način smo zagotovili, da naša koda pričakuje in dela s podatki pravilnega tipa, kar nam pomaga preprečevati morebitne napake.

Nekaterih tipov v PHP ni mogoče neposredno zapisati. V takem primeru se navedejo v phpDoc komentarju, kar je standardni format za dokumentiranje PHP kode, ki se začne z /** in konča z */. Omogoča dodajanje opisov razredov, metod itd. In tudi navajanje kompleksnih tipov s pomočjo t.i. anotacij @var, @param in @return. Te tipe nato uporabljajo orodja za statično analizo kode, vendar jih sam PHP ne preverja.

class Seznam
{
	/** @var array<Oseba> zapis pravi, da gre za polje objektov Oseba */
	private array $osebe = [];

	public function dodajOsebo(Oseba $oseba): void
	{
		$this->osebe[] = $oseba;
	}
}

Primerjava in identiteta

V PHP lahko objekte primerjate na dva načina:

  1. Primerjava vrednosti ==: Preveri, ali sta objekta istega razreda in imata enake vrednosti v svojih lastnostih.
  2. Identiteta ===: Preveri, ali gre za isto instanco objekta.
class Avto
{
	public string $znamka;

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

$avto1 = new Avto('Skoda');
$avto2 = new Avto('Skoda');
$avto3 = $avto1;

var_dump($avto1 == $avto2);   // true, ker imata enako vrednost
var_dump($avto1 === $avto2);  // false, ker nista ista instanca
var_dump($avto1 === $avto3);  // true, ker je $avto3 ista instanca kot $avto1

Operator instanceof

Operator instanceof omogoča ugotoviti, ali je dani objekt instanca določenega razreda, potomec tega razreda, ali pa implementira določen vmesnik.

Predstavljajmo si, da imamo razred Oseba in drug razred Student, ki je potomec razreda Oseba:

class Oseba
{
	private int $starost;

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

class Student extends Oseba
{
	private string $smer;

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

$student = new Student(20, 'Informatika');

// Preverjanje, ali je $student instanca razreda Student
var_dump($student instanceof Student);  // Izpis: bool(true)

// Preverjanje, ali je $student instanca razreda Oseba (ker je Student potomec Osebe)
var_dump($student instanceof Osoba);     // Izpis: bool(true)

Iz izpisov je razvidno, da se objekt $student hkrati šteje za instanco obeh razredov – Student in Oseba.

Tekoči vmesniki

“Tekoči vmesnik” (angleško “Fluent Interface”) je tehnika v OOP, ki omogoča veriženje metod skupaj v enem klicu. S tem se pogosto poenostavi in naredi koda bolj pregledna.

Ključni element tekočega vmesnika je, da vsaka metoda v verigi vrne referenco na trenutni objekt. To dosežemo tako, da na koncu metode uporabimo return $this;. Ta slog programiranja je pogosto povezan z metodami, imenovanimi “setters”, ki nastavljajo vrednosti lastnosti objekta.

Pokažimo si, kako lahko izgleda tekoči vmesnik na primeru pošiljanja e-pošte:

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

V tem primeru metode setFrom(), setRecipient() in setMessage() služijo za nastavitev ustreznih vrednosti (pošiljatelja, prejemnika, vsebine sporočila). Po nastavitvi vsake od teh vrednosti nam metode vrnejo trenutni objekt ($email), kar nam omogoča veriženje naslednje metode za njo. Na koncu kličemo metodo send(), ki e-pošto dejansko pošlje.

Zahvaljujoč tekočim vmesnikom lahko pišemo kodo, ki je intuitivna in lahko berljiva.

Kopiranje s clone

V PHP lahko ustvarimo kopijo objekta z uporabo operatorja clone. Na ta način dobimo novo instanco z identično vsebino.

Če moramo pri kopiranju objekta prilagoditi nekatere njegove lastnosti, lahko v razredu definiramo posebno metodo __clone(). Ta metoda se samodejno pokliče, ko je objekt kloniran.

class Ovca
{
	public string $ime;

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

	public function __clone()
	{
		$this->ime = 'Klon ' . $this->ime;
	}
}

$original = new Ovca('Dolly');
echo $original->ime . "\n";  // Izpiše: Dolly

$klon = clone $original;
echo $klon->ime . "\n";      // Izpiše: Klon Dolly

V tem primeru imamo razred Ovca z eno lastnostjo $ime. Ko kloniramo instanco tega razreda, metoda __clone() poskrbi, da ime klonirane ovce dobi predpono “Klon”.

Lastnosti (Traits)

Lastnosti (Traits) v PHP so orodje, ki omogoča deljenje metod, lastnosti in konstant med razredi ter preprečuje podvajanje kode. Lahko si jih predstavljate kot mehanizem “kopiraj in prilepi” (Ctrl-C in Ctrl-V), kjer se vsebina lastnosti “vstavi” v razrede. To vam omogoča ponovno uporabo kode brez potrebe po ustvarjanju zapletenih hierarhij razredov.

Poglejmo si preprost primer, kako uporabljati lastnosti v PHP:

trait Trobljenje
{
	public function potrobi()
	{
		echo 'Bip bip!';
	}
}

class Avto
{
	use Trobljenje;
}

class Tovornjak
{
	use Trobljenje;
}

$avto = new Avto;
$avto->potrobi(); // Izpiše 'Bip bip!'

$tovornjak = new Tovornjak;
$tovornjak->potrobi(); // Prav tako izpiše 'Bip bip!'

V tem primeru imamo lastnost, imenovano Trobljenje, ki vsebuje eno metodo potrobi(). Nato imamo dva razreda: Avto in Tovornjak, ki oba uporabljata lastnost Trobljenje. Zahvaljujoč temu oba razreda “imata” metodo potrobi(), in jo lahko kličemo na objektih obeh razredov.

Lastnosti vam omogočajo enostavno in učinkovito deljenje kode med razredi. Pri tem ne vstopajo v dedno hierarhijo, tj. $avto instanceof Trobljenje vrne false.

Izjeme

Izjeme v OOP nam omogočajo elegantno obravnavanje napak in nepričakovanih situacij v naši kodi. So objekti, ki nosijo informacije o napaki ali nenavadni situaciji.

V PHP imamo vgrajen razred Exception, ki služi kot osnova za vse izjeme. Ta ima več metod, ki nam omogočajo pridobiti več informacij o izjemi, kot so sporočilo o napaki, datoteka in vrstica, kjer je prišlo do napake, itd.

Ko v kodi pride do napake, lahko “sprožimo” izjemo z uporabo ključne besede throw.

function deljenje(float $a, float $b): float
{
	if ($b === 0.0) {
		throw new Exception('Deljenje z nič!');
	}
	return $a / $b;
}

Ko funkcija deljenje() dobi kot drugi argument ničlo, sproži izjemo s sporočilom o napaki 'Deljenje z nič!'. Da preprečimo sesutje programa ob sprožitvi izjeme, jo ujamemo v bloku try/catch:

try {
	echo deljenje(10, 0.0);
} catch (Exception $e) {
	echo 'Izjema ujeta: '. $e->getMessage();
}

Koda, ki lahko sproži izjemo, je zavita v blok try. Če je izjema sprožena, se izvajanje kode premakne v blok catch, kjer lahko izjemo obdelamo (npr. izpišemo sporočilo o napaki).

Po blokih try in catch lahko dodamo neobvezen blok finally, ki se izvede vedno, ne glede na to, ali je bila izjema sprožena ali ne (tudi v primeru, da v bloku try ali catch uporabimo ukaz return, break ali continue):

try {
	echo deljenje(10, 0.0);
} catch (Exception $e) {
	echo 'Izjema ujeta: '. $e->getMessage();
} finally {
	// Koda, ki se izvede vedno, ne glede na to, ali je bila izjema sprožena ali ne
}

Lahko ustvarimo tudi lastne razrede (hierarhijo) izjem, ki dedujejo od razreda Exception. Kot primer si predstavljajmo preprosto bančno aplikacijo, ki omogoča izvajanje pologov in dvigov:

class BancnaIzjema extends Exception {}
class PomanjkanjeSredstevIzjema extends BancnaIzjema {}
class PrekoracitevOmejitveIzjema extends BancnaIzjema {}

class BancniRacun
{
	private int $stanje = 0;
	private int $dnevnaOmejitev = 1000;

	public function poloziti(int $znesek): int
	{
		$this->stanje += $znesek;
		return $this->stanje;
	}

	public function dvigniti(int $znesek): int
	{
		if ($znesek > $this->stanje) {
			throw new PomanjkanjeSredstevIzjema('Na računu ni dovolj sredstev.');
		}

		if ($znesek > $this->dnevnaOmejitev) {
			throw new PrekoracitevOmejitveIzjema('Dnevna omejitev dvigov je bila presežena.');
		}

		$this->stanje -= $znesek;
		return $this->stanje;
	}
}

Za en blok try lahko navedemo več blokov catch, če pričakujete različne vrste izjem.

$racun = new BancniRacun;
$racun->poloziti(500);

try {
	$racun->dvigniti(1500);
} catch (PrekoracitevOmejitveIzjema $e) {
	echo $e->getMessage();
} catch (PomanjkanjeSredstevIzjema $e) {
	echo $e->getMessage();
} catch (BancnaIzjema $e) {
	echo 'Pri izvajanju operacije je prišlo do napake.';
}

V tem primeru je pomemben vrstni red blokov catch. Ker vse izjeme dedujejo od BancnaIzjema, če bi imeli ta blok prvi, bi se v njem ujele vse izjeme, ne da bi koda prišla do naslednjih catch blokov. Zato je pomembno imeti bolj specifične izjeme (tj. tiste, ki dedujejo od drugih) v bloku catch višje v vrstnem redu kot njihove starševske izjeme.

Iteracija

V PHP lahko prehajate skozi objekte z uporabo foreach zanke, podobno kot prehajate skozi polja. Da bi to delovalo, mora objekt implementirati poseben vmesnik.

Prva možnost je implementirati vmesnik Iterator, ki ima metode current() za vračanje trenutne vrednosti, key() za vračanje ključa, next() za premik na naslednjo vrednost, rewind() za premik na začetek in valid() za ugotavljanje, ali še nismo na koncu.

Druga možnost je implementirati vmesnik IteratorAggregate, ki ima samo eno metodo getIterator(). Ta bodisi vrne nadomestni objekt, ki bo zagotavljal prehajanje, ali pa lahko predstavlja generator, kar je posebna funkcija, v kateri se uporablja yield za postopno vračanje ključev in vrednosti:

class Oseba
{
	public function __construct(
		public int $starost,
	) {
	}
}

class Seznam implements IteratorAggregate
{
	private array $osebe = [];

	public function dodajOsebo(Oseba $oseba): void
	{
		$this->osebe[] = $oseba;
	}

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

$seznam = new Seznam;
$seznam->dodajOsebo(new Oseba(30));
$seznam->dodajOsebo(new Oseba(25));

foreach ($seznam as $oseba) {
	echo "Starost: {$oseba->starost} let \n";
}

Dobre prakse

Ko imate za sabo osnovna načela objektno usmerjenega programiranja, je pomembno, da se osredotočite na dobre prakse v OOP. Te vam bodo pomagale pisati kodo, ki ni samo funkcionalna, ampak tudi berljiva, razumljiva in enostavno vzdrževana.

  1. Ločevanje odgovornosti (Separation of Concerns): Vsak razred bi moral imeti jasno definirano odgovornost in bi moral reševati samo eno glavno nalogo. Če razred počne preveč stvari, ga je morda primerno razdeliti na manjše, specializirane razrede.
  2. Inkapsulacija (Encapsulation): Podatki in metode bi morali biti čim bolj skriti in dostopni samo preko definiranega vmesnika. To vam omogoča spreminjanje interne implementacije razreda brez vpliva na preostalo kodo.
  3. Vnašanje odvisnosti (Dependency Injection): Namesto da bi ustvarili odvisnosti neposredno v razredu, bi jih morali “vnašati” od zunaj. Za globlje razumevanje tega načela priporočamo poglavja o vnašanju odvisnosti.
različica: 4.0