Bevezetés az objektumorientált programozásba

Az “OOP” kifejezés az objektumorientált programozást jelenti, amely a kód szervezésének és strukturálásának egy módja. Az OOP lehetővé teszi számunkra, hogy a programot egymással kommunikáló objektumok halmazaként tekintsük, nem pedig utasítások és függvények sorozataként.

Az OOP-ban az “objektum” egy olyan egység, amely adatokat és azokkal az adatokkal dolgozó függvényeket tartalmaz. Az objektumok “osztályok” alapján jönnek létre, amelyeket az objektumok terveiként vagy sablonjaiként foghatunk fel. Ha van egy osztályunk, létrehozhatjuk annak “példányát”, ami egy konkrét, az osztály alapján létrehozott objektum.

Nézzük meg, hogyan hozhatunk létre egy egyszerű osztályt PHP-ban. Az osztály definiálásakor a “class” kulcsszót használjuk, amelyet az osztály neve követ, majd kapcsos zárójelek, amelyek az osztály függvényeit (ezeket “metódusoknak” nevezzük) és változóit (ezeket “property-knek” vagy “tulajdonságoknak” nevezzük) foglalják magukba:

class Auto
{
	function dudal()
	{
		echo 'Bip bip!';
	}
}

Ebben a példában létrehoztunk egy Auto nevű osztályt egyetlen dudal nevű függvénnyel (vagy “metódussal”).

Minden osztálynak csak egy fő feladatot kellene megoldania. Ha egy osztály túl sok mindent csinál, érdemes lehet kisebb, specializált osztályokra bontani.

Az osztályokat általában külön fájlokba mentjük, hogy a kód rendezett és könnyen áttekinthető legyen. A fájl nevének meg kell egyeznie az osztály nevével, tehát az Auto osztály esetében a fájl neve Auto.php lenne.

Az osztályok elnevezésekor jó gyakorlat a “PascalCase” konvenció követése, ami azt jelenti, hogy a név minden szava nagybetűvel kezdődik, és nincsenek közöttük aláhúzások vagy más elválasztójelek. A metódusok és property-k a “camelCase” konvenció szerint kisbetűvel kezdődnek.

Néhány PHP metódusnak speciális feladata van, és __ (két aláhúzás) előtaggal vannak jelölve. Az egyik legfontosabb speciális metódus a “konstruktor”, amelyet __construct-ként jelölünk. A konstruktor egy olyan metódus, amely automatikusan meghívódik, amikor létrehozunk egy új osztálypéldányt.

A konstruktort gyakran használjuk az objektum kezdeti állapotának beállítására. Például, amikor egy személyt reprezentáló objektumot hozunk létre, a konstruktort használhatjuk a korának, nevének vagy más tulajdonságainak beállítására.

Nézzük meg, hogyan használhatunk konstruktort PHP-ban:

class Szemely
{
	private $kor;

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

	function hanyEvesVagy()
	{
		return $this->kor;
	}
}

$szemely = new Szemely(25);
echo $szemely->hanyEvesVagy(); // Kiírja: 25

Ebben a példában a Szemely osztálynak van egy $kor property-je (változója) és egy konstruktora, amely ezt a property-t állítja be. A hanyEvesVagy() metódus ezután lehetővé teszi a személy korához való hozzáférést.

A $this pszeudo-változót az osztályon belül használjuk az objektum property-jeihez és metódusaihoz való hozzáféréshez.

A new kulcsszót használjuk egy új osztálypéldány létrehozásához. A fenti példában egy új, 25 éves személyt hoztunk létre.

Beállíthatunk alapértelmezett értékeket is a konstruktor paramétereinek, ha azokat nem adjuk meg az objektum létrehozásakor. Például:

class Szemely
{
	private $kor;

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

	function hanyEvesVagy()
	{
		return $this->kor;
	}
}

$szemely = new Szemely;  // ha nem adunk át argumentumot, a zárójelek elhagyhatók
echo $szemely->hanyEvesVagy(); // Kiírja: 20

Ebben a példában, ha nem adja meg a kort a Szemely objektum létrehozásakor, az alapértelmezett 20-as érték lesz használva.

Kellemes, hogy a property definíciója és annak konstruktoron keresztüli inicializálása így lerövidíthető és egyszerűsíthető:

class Szemely
{
	function __construct(
		private $kor = 20,
	) {
	}
}

A teljesség kedvéért, a konstruktorok mellett az objektumoknak lehetnek destruktoraik is (a __destruct metódus), amelyek azelőtt hívódnak meg, mielőtt az objektum felszabadulna a memóriából.

Névterek

A névterek (angolul “namespaces”) lehetővé teszik számunkra, hogy a kapcsolódó osztályokat, függvényeket és konstansokat szervezzük és csoportosítsuk, miközben elkerüljük a névütközéseket. Úgy képzelhetjük el őket, mint mappákat a számítógépen, ahol minden mappa egy adott projekthez vagy témához tartozó fájlokat tartalmaz.

A névterek különösen hasznosak nagyobb projektekben, vagy amikor harmadik féltől származó könyvtárakat használunk, ahol osztálynév-ütközések léphetnek fel.

Képzeljük el, hogy van egy Auto nevű osztályunk a projektünkben, és szeretnénk azt egy Doprava nevű névtérbe helyezni. Ezt így tehetjük meg:

namespace Doprava;

class Auto
{
	function dudal()
	{
		echo 'Bip bip!';
	}
}

Ha az Auto osztályt egy másik fájlban szeretnénk használni, meg kell adnunk, hogy melyik névtérből származik az osztály:

$auto = new Doprava\Auto;

Az egyszerűsítés érdekében a fájl elején megadhatjuk, hogy melyik osztályt szeretnénk használni az adott névtérből, ami lehetővé teszi a példányok létrehozását anélkül, hogy a teljes elérési utat meg kellene adni:

use Doprava\Auto;

$auto = new Auto;

Öröklődés

Az öröklődés az objektumorientált programozás egyik eszköze, amely lehetővé teszi új osztályok létrehozását már létező osztályok alapján, átvéve azok property-jeit és metódusait, és szükség szerint kiterjesztve vagy újradefiniálva azokat. Az öröklődés lehetővé teszi a kód újrafelhasználhatóságát és az osztályhierarchiát.

Egyszerűen fogalmazva, ha van egy osztályunk, és szeretnénk egy másikat létrehozni, amely abból származik, de néhány változtatással, akkor az új osztályt “örökölhetjük” az eredeti osztályból.

PHP-ban az öröklődést az extends kulcsszóval valósítjuk meg.

A Szemely osztályunk tárolja a kor információt. Lehet egy másik Diak osztályunk, amely kiterjeszti a Szemely-t, és hozzáadja a tanulmányi szak információját.

Nézzünk egy példát:

class Szemely
{
	private $kor;

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

	function informacioKiirasa()
	{
		echo "Kor: {$this->kor} év\n";
	}
}

class Diak extends Szemely
{
	private $szak;

	function __construct($kor, $szak)
	{
		parent::__construct($kor);
		$this->szak = $szak;
	}

	function informacioKiirasa()
	{
		parent::informacioKiirasa();
		echo "Tanulmányi szak: {$this->szak} \n";
	}
}

$diak = new Diak(20, 'Informatika');
$diak->informacioKiirasa();

Hogyan működik ez a kód?

  • Az extends kulcsszót használtuk a Szemely osztály kiterjesztéséhez, ami azt jelenti, hogy a Diak osztály örökli az összes metódust és property-t a Szemely-től.
  • A parent:: kulcsszó lehetővé teszi számunkra, hogy metódusokat hívjunk a szülő osztályból. Ebben az esetben a Szemely osztály konstruktorát hívtuk meg, mielőtt saját funkcionalitást adtunk volna hozzá a Diak osztályhoz. És hasonlóképpen az ős informacioKiirasa() metódusát is, mielőtt kiírtuk volna a diákra vonatkozó információkat.

Az öröklődés olyan helyzetekre való, amikor “egy” kapcsolat (is-a relationship) áll fenn az osztályok között. Például a Diak egy Szemely. A macska egy állat. Lehetőséget ad nekünk arra, hogy olyan esetekben, amikor a kódban egy objektumot (pl. “Szemely”) várunk, helyette egy öröklött objektumot (pl. “Diak”) használjunk.

Fontos megjegyezni, hogy az öröklődés fő célja nem a kódduplikáció elkerülése. Éppen ellenkezőleg, az öröklődés helytelen használata bonyolult és nehezen karbantartható kódhoz vezethet. Ha az “egy” kapcsolat nem létezik az osztályok között, az öröklődés helyett a kompozíciót kellene fontolóra vennünk.

Vegyük észre, hogy a Szemely és Diak osztályok informacioKiirasa() metódusai kissé eltérő információkat írnak ki. És hozzáadhatunk további osztályokat (például Alkalmazott), amelyek további implementációkat biztosítanak ehhez a metódushoz. Azt a képességet, hogy különböző osztályok objektumai ugyanarra a metódusra különböző módon reagáljanak, polimorfizmusnak nevezzük:

$szemelyek = [
	new Szemely(30),
	new Diak(20, 'Informatika'),
	new Alkalmazott(45, 'Igazgató'),
];

foreach ($szemelyek as $szemely) {
	$szemely->informacioKiirasa();
}

Kompozíció

A kompozíció egy olyan technika, amikor ahelyett, hogy egy másik osztály property-jeit és metódusait örökölnénk, egyszerűen felhasználjuk annak példányát a saját osztályunkban. Ez lehetővé teszi számunkra, hogy több osztály funkcionalitását és property-jeit kombináljuk anélkül, hogy bonyolult öröklődési struktúrákat kellene létrehoznunk.

Nézzünk egy példát. Van egy Motor osztályunk és egy Auto osztályunk. Ahelyett, hogy azt mondanánk “Az Autó egy Motor”, azt mondjuk “Az Autónak van Motorja”, ami egy tipikus kompozíciós kapcsolat.

class Motor
{
	function bekapcsol()
	{
		echo 'Motor fut.';
	}
}

class Auto
{
	private $motor;

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

	function indit()
	{
		$this->motor->bekapcsol();
		echo 'Az autó készen áll az indulásra!';
	}
}

$auto = new Auto;
$auto->indit();

Itt az Auto nem rendelkezik a Motor összes property-jével és metódusával, de hozzáfér hozzá a $motor property-n keresztül.

A kompozíció előnye a nagyobb tervezési flexibilitás és a jövőbeli módosítások jobb lehetősége.

Láthatóság

PHP-ban definiálhatunk “láthatóságot” az osztály property-jeire, metódusaira és konstansaira. A láthatóság határozza meg, hogy honnan férhetünk hozzá ezekhez az elemekhez.

  1. Public: Ha egy elem public-ként van megjelölve, az azt jelenti, hogy bárhonnan hozzáférhetünk, akár az osztályon kívülről is.
  2. Protected: A protected jelölésű elem csak az adott osztályon belül és annak minden leszármazottjában (azok az osztályok, amelyek ebből az osztályból örökölnek) érhető el.
  3. Private: Ha egy elem private, akkor csak azon az osztályon belülről férhetünk hozzá, amelyben definiálva lett.

Ha nem adunk meg láthatóságot, a PHP automatikusan public-ra állítja.

Nézzünk egy példakódot:

class LathatosagDemonstracio
{
	public $nyilvanosTulajdonsag = 'Nyilvános';
	protected $vedettTulajdonsag = 'Védett';
	private $privatTulajdonsag = 'Privát';

	public function tulajdonsagokKiirasa()
	{
		echo $this->nyilvanosTulajdonsag;  // Működik
		echo $this->vedettTulajdonsag; // Működik
		echo $this->privatTulajdonsag; // Működik
	}
}

$objektum = new LathatosagDemonstracio;
$objektum->tulajdonsagokKiirasa();
echo $objektum->nyilvanosTulajdonsag;      // Működik
// echo $objektum->vedettTulajdonsag;  // Hibát dob
// echo $objektum->privatTulajdonsag;  // Hibát dob

Folytassuk az osztály öröklésével:

class OsztalyLeszarmazott extends LathatosagDemonstracio
{
	public function tulajdonsagokKiirasa()
	{
		echo $this->nyilvanosTulajdonsag;   // Működik
		echo $this->vedettTulajdonsag;  // Működik
		// echo $this->privatTulajdonsag;  // Hibát dob
	}
}

Ebben az esetben az OsztalyLeszarmazott osztály tulajdonsagokKiirasa() metódusa hozzáférhet a nyilvános és védett property-khez, de nem férhet hozzá a szülő osztály privát property-jeihez.

Az adatokat és metódusokat a lehető legjobban el kell rejteni, és csak egy definiált interfészen keresztül szabad hozzáférni hozzájuk. Ez lehetővé teszi az osztály belső implementációjának megváltoztatását anélkül, hogy a kód többi részét befolyásolná.

A final kulcsszó

PHP-ban használhatjuk a final kulcsszót, ha meg akarjuk akadályozni egy osztály, metódus vagy konstans öröklését vagy felülírását. Ha egy osztályt final-ként jelölünk meg, nem lehet kiterjeszteni. Ha egy metódust final-ként jelölünk meg, azt nem lehet felülírni egy leszármazott osztályban.

Annak tudata, hogy egy bizonyos osztály vagy metódus nem lesz tovább módosítva, lehetővé teszi számunkra, hogy könnyebben végezzünk módosításokat anélkül, hogy aggódnunk kellene a lehetséges konfliktusok miatt. Például hozzáadhatunk egy új metódust anélkül, hogy attól tartanánk, hogy valamelyik leszármazottjának már van egy azonos nevű metódusa, és ütközés történne. Vagy megváltoztathatjuk egy metódus paramétereit, mivel itt sem fenyeget az a veszély, hogy ellentmondást okozunk egy leszármazottban felülírt metódussal.

final class VeglegesOsztaly
{
}

// A következő kód hibát okoz, mert nem örökölhetünk a final osztályból.
class VeglegesOsztalyLeszarmazott extends VeglegesOsztaly
{
}

Ebben a példában a VeglegesOsztaly final osztályból való öröklési kísérlet hibát okoz.

Statikus property-k és metódusok

Amikor PHP-ban “statikus” osztályelemekről beszélünk, olyan metódusokra és property-kre gondolunk, amelyek magához az osztályhoz tartoznak, nem pedig az osztály egy konkrét példányához. Ez azt jelenti, hogy nem kell létrehoznia az osztály egy példányát ahhoz, hogy hozzáférjen hozzájuk. Ehelyett közvetlenül az osztály nevén keresztül hívja meg vagy éri el őket.

Ne feledje, hogy mivel a statikus elemek az osztályhoz tartoznak, és nem annak példányaihoz, a statikus metódusokon belül nem használhatja a $this pszeudo-változót.

A statikus property-k használata átláthatatlan, buktatókkal teli kódhoz vezet, ezért soha ne használja őket, és itt nem is mutatunk példát a használatukra. Ezzel szemben a statikus metódusok hasznosak. Példa a használatra:

class Szamologep
{
	public static function osszeadas($a, $b)
	{
		return $a + $b;
	}

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

// Statikus metódus használata az osztály példányosítása nélkül
echo Szamologep::osszeadas(5, 3); // Eredmény: 8
echo Szamologep::kivonas(5, 3); // Eredmény: 2

Ebben a példában létrehoztunk egy Szamologep osztályt két statikus metódussal. Ezeket a metódusokat közvetlenül, az osztály példányának létrehozása nélkül hívhatjuk meg a :: operátor segítségével. A statikus metódusok különösen hasznosak olyan műveletekhez, amelyek nem függenek egy konkrét osztálypéldány állapotától.

Osztálykonstansok

Az osztályokon belül lehetőségünk van konstansok definiálására. A konstansok olyan értékek, amelyek soha nem változnak a program futása során. A változókkal ellentétben a konstans értéke mindig ugyanaz marad.

class Auto
{
	public const KerekekSzama = 4;

	public function kerekekSzamaMegjelenitese(): int
	{
		echo self::KerekekSzama;
	}
}

echo Auto::KerekekSzama;  // Kimenet: 4

Ebben a példában van egy Auto osztályunk a KerekekSzama konstanssal. Ha az osztályon belül szeretnénk hozzáférni a konstanshoz, az osztály neve helyett a self kulcsszót használhatjuk.

Objektuminterfészek

Az objektuminterfészek “szerződésekként” működnek az osztályok számára. Ha egy osztálynak implementálnia kell egy objektuminterfészt, tartalmaznia kell az összes metódust, amelyet ez az interfész definiál. Ez egy nagyszerű módja annak biztosítására, hogy bizonyos osztályok ugyanazt a “szerződést” vagy struktúrát kövessék.

PHP-ban az interfészt az interface kulcsszóval definiáljuk. Az interfészben definiált összes metódus public (public). Amikor egy osztály implementál egy interfészt, az implements kulcsszót használja.

interface Allat
{
	function hangotAd();
}

class Macska implements Allat
{
	public function hangotAd()
	{
		echo 'Mňau';
	}
}

$macska = new Macska;
$macska->hangotAd();

Ha egy osztály implementál egy interfészt, de nem definiálja az összes elvárt metódust, a PHP hibát dob.

Egy osztály egyszerre több interfészt is implementálhat, ami eltérés az öröklődéstől, ahol egy osztály csak egyetlen osztálytól örökölhet:

interface Orzo
{
	function hazatOriz();
}

class Kutya implements Allat, Orzo
{
	public function hangotAd()
	{
		echo 'Haf';
	}

	public function hazatOriz()
	{
		echo 'A kutya gondosan őrzi a házat';
	}
}

Absztrakt osztályok

Az absztrakt osztályok alap sablonként szolgálnak más osztályok számára, de nem hozhat létre közvetlenül példányokat belőlük. Tartalmaznak teljes metódusok és absztrakt metódusok kombinációját, amelyeknek nincs definiált tartalmuk. Azok az osztályok, amelyek absztrakt osztályokból örökölnek, meg kell adniuk az összes absztrakt metódus definícióját az ősből.

Absztrakt osztály definiálásához az abstract kulcsszót használjuk.

abstract class AbsztraktOsztaly
{
	public function normalMetodus()
	{
		echo 'Ez egy normál metódus';
	}

	abstract public function absztraktMetodus();
}

class Leszarmazott extends AbsztraktOsztaly
{
	public function absztraktMetodus()
	{
		echo 'Ez az absztrakt metódus implementációja';
	}
}

$peldany = new Leszarmazott;
$peldany->normalMetodus();
$peldany->absztraktMetodus();

Ebben a példában van egy absztrakt osztályunk egy normál és egy absztrakt metódussal. Ezután van egy Leszarmazott osztályunk, amely az AbsztraktOsztaly-ból örököl, és implementációt biztosít az absztrakt metódushoz.

Miben különböznek valójában az interfészek és az absztrakt osztályok? Az absztrakt osztályok tartalmazhatnak absztrakt és konkrét metódusokat is, míg az interfészek csak azt definiálják, hogy egy osztálynak milyen metódusokat kell implementálnia, de nem nyújtanak semmilyen implementációt. Egy osztály csak egy absztrakt osztálytól örökölhet, de tetszőleges számú interfészt implementálhat.

Típusellenőrzés

A programozásban nagyon fontos biztosnak lenni abban, hogy az adatok, amelyekkel dolgozunk, a megfelelő típusúak. PHP-ban vannak eszközeink, amelyek ezt biztosítják számunkra. Annak ellenőrzését, hogy az adatok megfelelő típusúak-e, “típusellenőrzésnek” nevezzük.

Típusok, amelyekkel PHP-ban találkozhatunk:

  1. Alaptípusok: Ide tartoznak az int (egész számok), float (lebegőpontos számok), bool (logikai értékek), string (karakterláncok), array (tömbök) és null.
  2. Osztályok: Ha azt szeretnénk, hogy az érték egy specifikus osztály példánya legyen.
  3. Interfészek: Metódusok halmazát definiálja, amelyeket egy osztálynak implementálnia kell. Az interfésznek megfelelő értéknek rendelkeznie kell ezekkel a metódusokkal.
  4. Vegyes típusok: Meghatározhatjuk, hogy egy változó több megengedett típussal is rendelkezhet.
  5. Void: Ez a speciális típus azt jelzi, hogy egy függvény vagy metódus nem ad vissza értéket.

Nézzük meg, hogyan módosíthatjuk a kódot, hogy tartalmazza a típusokat:

class Szemely
{
	private int $kor;

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

	public function korKiirasa(): void
	{
		echo "Ez a személy {$this->kor} éves.";
	}
}

/**
 * Függvény, amely egy Szemely osztály objektumát fogadja el és kiírja a személy korát.
 */
function szemelyKoranakKiirasa(Szemely $szemely): void
{
	$szemely->korKiirasa();
}

Ezzel a módszerrel biztosítottuk, hogy a kódunk a megfelelő típusú adatokat várja és dolgozza fel, ami segít megelőzni a potenciális hibákat.

Néhány típust nem lehet közvetlenül PHP-ban leírni. Ebben az esetben a phpDoc kommentben adjuk meg, ami a PHP kód dokumentálásának szabványos formátuma, /**-el kezdődik és */-el végződik. Lehetővé teszi osztályok, metódusok stb. leírásának hozzáadását. És komplex típusok megadását is ún. annotációk @var, @param és @return segítségével. Ezeket a típusokat aztán a statikus kódelemző eszközök használják, de maga a PHP nem ellenőrzi őket.

class Lista
{
	/** @var array<Szemely>  a jelölés azt mondja, hogy Szemely objektumok tömbjéről van szó */
	private array $szemelyek = [];

	public function szemelyHozzaadasa(Szemely $szemely): void
	{
		$this->szemelyek[] = $szemely;
	}
}

Összehasonlítás és azonosság

PHP-ban kétféleképpen hasonlíthatunk össze objektumokat:

  1. Érték szerinti összehasonlítás ==: Ellenőrzi, hogy az objektumok azonos osztályúak-e és azonos értékekkel rendelkeznek-e a property-jeikben.
  2. Azonosság ===: Ellenőrzi, hogy ugyanarról az objektumpéldányról van-e szó.
class Auto
{
	public string $marka;

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

$auto1 = new Auto('Skoda');
$auto2 = new Auto('Skoda');
$auto3 = $auto1;

var_dump($auto1 == $auto2);   // true, mert azonos értékűek
var_dump($auto1 === $auto2);  // false, mert nem ugyanaz a példány
var_dump($auto1 === $auto3);  // true, mert $auto3 ugyanaz a példány, mint $auto1

Az instanceof operátor

Az instanceof operátor lehetővé teszi annak megállapítását, hogy egy adott objektum egy bizonyos osztály példánya-e, ennek az osztálynak a leszármazottja-e, vagy implementál-e egy bizonyos interfészt.

Képzeljük el, hogy van egy Szemely osztályunk és egy másik Diak osztályunk, amely a Szemely osztály leszármazottja:

class Szemely
{
	private int $kor;

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

class Diak extends Szemely
{
	private string $szak;

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

$diak = new Diak(20, 'Informatika');

// Ellenőrzés, hogy $diak a Diak osztály példánya-e
var_dump($diak instanceof Diak);  // Kimenet: bool(true)

// Ellenőrzés, hogy $diak a Szemely osztály példánya-e (mivel a Diak a Szemely leszármazottja)
var_dump($diak instanceof Szemely);     // Kimenet: bool(true)

A kimenetekből látható, hogy a $diak objektum egyszerre mindkét osztály – Diak és Szemely – példányának tekintendő.

Fluent Interfészek

A “Fluent Interface” (magyarul “Fluent Interfész”) egy technika az OOP-ban, amely lehetővé teszi a metódusok láncolását egyetlen hívásban. Ez gyakran egyszerűsíti és átláthatóbbá teszi a kódot.

A fluent interfész kulcsfontosságú eleme, hogy a lánc minden metódusa hivatkozást ad vissza az aktuális objektumra. Ezt úgy érjük el, hogy a metódus végén return $this;-t használunk. Ezt a programozási stílust gyakran “settereknek” nevezett metódusokkal társítják, amelyek az objektum property-jeinek értékeit állítják be.

Mutassuk be, hogyan nézhet ki egy fluent interfész egy e-mail küldési példán:

public function uzenetKuldese()
{
	$email = new Email;
	$email->setFelado('sender@example.com')
		  ->setCimzett('admin@example.com')
		  ->setUzenet('Hello, this is a message.')
		  ->kuldes();
}

Ebben a példában a setFelado(), setCimzett() és setUzenet() metódusok a megfelelő értékek (feladó, címzett, üzenet tartalma) beállítására szolgálnak. Miután beállítottuk mindegyik értéket, a metódusok visszaadják az aktuális objektumot ($email), ami lehetővé teszi, hogy egy másik metódust láncoljunk utána. Végül meghívjuk a kuldes() metódust, amely ténylegesen elküldi az e-mailt.

A fluent interfészeknek köszönhetően olyan kódot írhatunk, amely intuitív és könnyen olvasható.

Másolás a clone segítségével

PHP-ban létrehozhatunk egy objektum másolatát a clone operátor segítségével. Ezzel a módszerrel egy új, azonos tartalmú példányt kapunk.

Ha az objektum másolásakor módosítani kell néhány property-jét, definiálhatunk egy speciális __clone() metódust az osztályban. Ez a metódus automatikusan meghívódik, amikor az objektumot klónozzák.

class Barany
{
	public string $nev;

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

	public function __clone()
	{
		$this->nev = 'Klón ' . $this->nev;
	}
}

$eredeti = new Barany('Dolly');
echo $eredeti->nev . "\n";  // Kiírja: Dolly

$klon = clone $eredeti;
echo $klon->nev . "\n";      // Kiírja: Klón Dolly

Ebben a példában van egy Barany osztályunk egy $nev property-vel. Amikor klónozzuk ennek az osztálynak egy példányát, a __clone() metódus gondoskodik arról, hogy a klónozott bárány neve “Klón” előtagot kapjon.

Traitek

A traitek PHP-ban olyan eszközök, amelyek lehetővé teszik metódusok, property-k és konstansok megosztását osztályok között, és megakadályozzák a kódduplikációt. Úgy képzelhetjük el őket, mint egy “másolás és beillesztés” (Ctrl-C és Ctrl-V) mechanizmust, ahol a trait tartalma “beillesztődik” az osztályokba. Ez lehetővé teszi a kód újrafelhasználását anélkül, hogy bonyolult osztályhierarchiákat kellene létrehozni.

Nézzünk egy egyszerű példát a traitek használatára PHP-ban:

trait Dudalas
{
	public function dudal()
	{
		echo 'Bip bip!';
	}
}

class Auto
{
	use Dudalas;
}

class Teherauto
{
	use Dudalas;
}

$auto = new Auto;
$auto->dudal(); // Kiírja 'Bip bip!'

$teherauto = new Teherauto;
$teherauto->dudal(); // Szintén kiírja 'Bip bip!'

Ebben a példában van egy Dudalas nevű traitünk, amely egy dudal() metódust tartalmaz. Ezután van két osztályunk: Auto és Teherauto, amelyek mindketten használják a Dudalas traitet. Ennek köszönhetően mindkét osztály “rendelkezik” a dudal() metódussal, és meghívhatjuk azt mindkét osztály objektumain.

A traitek lehetővé teszik a kód egyszerű és hatékony megosztását az osztályok között. Eközben nem lépnek be az öröklődési hierarchiába, azaz $auto instanceof Dudalas false értéket ad vissza.

Kivételek

A kivételek az OOP-ban lehetővé teszik számunkra, hogy elegánsan kezeljük a hibákat és a váratlan helyzeteket a kódunkban. Ezek olyan objektumok, amelyek információt hordoznak a hibáról vagy a szokatlan helyzetről.

PHP-ban van egy beépített Exception osztályunk, amely az összes kivétel alapjául szolgál. Több metódusa van, amelyek lehetővé teszik számunkra, hogy több információt kapjunk a kivételről, például a hibaüzenetet, a fájlt és a sort, ahol a hiba történt, stb.

Amikor hiba történik a kódban, “dobhatunk” egy kivételt a throw kulcsszó segítségével.

function osztas(float $a, float $b): float
{
	if ($b === 0.0) { // Use float comparison
		throw new Exception('Nullával való osztás!');
	}
	return $a / $b;
}

Amikor az osztas() függvény nullát kap második argumentumként, kivételt dob a 'Nullával való osztás!' hibaüzenettel. Annak megakadályozására, hogy a program leálljon a kivétel dobásakor, elkapjuk azt egy try/catch blokkban:

try {
	echo osztas(10, 0);
} catch (Exception $e) {
	echo 'Kivétel elkapva: '. $e->getMessage();
}

A kódot, amely kivételt dobhat, egy try blokkba csomagoljuk. Ha kivétel dobódik, a kód végrehajtása átkerül a catch blokkba, ahol feldolgozhatjuk a kivételt (pl. kiírhatjuk a hibaüzenetet).

A try és catch blokkok után hozzáadhatunk egy opcionális finally blokkot, amely mindig lefut, függetlenül attól, hogy dobódott-e kivétel vagy sem (még akkor is, ha a try vagy catch blokkban return, break vagy continue utasítást használunk):

try {
	echo osztas(10, 0);
} catch (Exception $e) {
	echo 'Kivétel elkapva: '. $e->getMessage();
} finally {
	// Kód, amely mindig lefut, függetlenül attól, hogy dobódott-e kivétel
}

Létrehozhatunk saját kivételosztályokat (hierarchiát) is, amelyek az Exception osztálytól örökölnek. Példaként képzeljünk el egy egyszerű banki alkalmazást, amely lehetővé teszi befizetések és kifizetések végrehajtását:

class BankiKivetel extends Exception {}
class ElegtetlenFedezetKivetel extends BankiKivetel {}
class LimitTullepesKivetel extends BankiKivetel {}

class Bankszamla
{
	private int $egyenleg = 0;
	private int $napiLimit = 1000;

	public function befizet(int $osszeg): int
	{
		$this->egyenleg += $osszeg;
		return $this->egyenleg;
	}

	public function kifizet(int $osszeg): int
	{
		if ($osszeg > $this->egyenleg) {
			throw new ElegtetlenFedezetKivetel('Nincs elegendő fedezet a számlán.');
		}

		if ($osszeg > $this->napiLimit) {
			throw new LimitTullepesKivetel('Túllépték a napi kifizetési limitet.');
		}

		$this->egyenleg -= $osszeg;
		return $this->egyenleg;
	}
}

Egy try blokkhoz több catch blokkot is megadhatunk, ha különböző típusú kivételekre számítunk.

$szamla = new Bankszamla;
$szamla->befizet(500);

try {
	$szamla->kifizet(1500);
} catch (LimitTullepesKivetel $e) {
	echo $e->getMessage();
} catch (ElegtetlenFedezetKivetel $e) {
	echo $e->getMessage();
} catch (BankiKivetel $e) {
	echo 'Hiba történt a művelet végrehajtása során.';
}

Ebben a példában fontos megjegyezni a catch blokkok sorrendjét. Mivel minden kivétel a BankiKivetel-től örököl, ha ezt a blokkot tennénk elsőnek, az összes kivételt elkapná, anélkül, hogy a kód eljutna a következő catch blokkokhoz. Ezért fontos, hogy a specifikusabb kivételek (azaz azok, amelyek másoktól örökölnek) a catch blokkban magasabban legyenek a sorrendben, mint a szülő kivételeik.

Iteráció

PHP-ban objektumokon iterálhatunk a foreach ciklus segítségével, hasonlóan ahhoz, ahogy tömbökön iterálunk. Ahhoz, hogy ez működjön, az objektumnak implementálnia kell egy speciális interfészt.

Az első lehetőség az Iterator interfész implementálása, amelynek metódusai a current() (aktuális értéket adja vissza), key() (kulcsot adja vissza), next() (következő értékre lép), rewind() (kezdetre lép) és valid() (ellenőrzi, hogy még nem értünk-e a végére).

A második lehetőség az IteratorAggregate interfész implementálása, amelynek csak egyetlen getIterator() metódusa van. Ez vagy egy helyettesítő objektumot ad vissza, amely biztosítja az iterációt, vagy lehet egy generátor, ami egy speciális függvény, amelyben a yield-et használjuk a kulcsok és értékek fokozatos visszaadására:

class Szemely
{
	public function __construct(
		public int $kor,
	) {
	}
}

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

	public function szemelyHozzaadasa(Szemely $szemely): void
	{
		$this->szemelyek[] = $szemely;
	}

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

$lista = new Lista;
$lista->szemelyHozzaadasa(new Szemely(30));
$lista->szemelyHozzaadasa(new Szemely(25));

foreach ($lista as $szemely) {
	echo "Kor: {$szemely->kor} év \n";
}

Helyes gyakorlatok

Miután elsajátította az objektumorientált programozás alapelveit, fontos a helyes OOP gyakorlatokra összpontosítani. Ezek segítenek olyan kódot írni, amely nemcsak funkcionális, hanem olvasható, érthető és könnyen karbantartható is.

  1. Felelősségek szétválasztása (Separation of Concerns): Minden osztálynak világosan meghatározott felelősséggel kell rendelkeznie, és csak egy fő feladatot kell megoldania. Ha egy osztály túl sok mindent csinál, érdemes lehet kisebb, specializált osztályokra bontani.
  2. Egységbezárás (Encapsulation): Az adatokat és metódusokat a lehető legjobban el kell rejteni, és csak egy definiált interfészen keresztül szabad hozzáférni hozzájuk. Ez lehetővé teszi az osztály belső implementációjának megváltoztatását anélkül, hogy a kód többi részét befolyásolná.
  3. Függőséginjektálás (Dependency Injection): Ahelyett, hogy a függőségeket közvetlenül az osztályban hozná létre, azokat kívülről kellene “injektálni”. Ennek az elvnek a mélyebb megértéséhez javasoljuk a Dependency Injection fejezetek elolvasását.
verzió: 4.0