Bevezetés az objektumorientált programozásba

Az “OOP” kifejezés az objektumorientált programozás rövidítése, amely a kód szervezésének és strukturálásának egy módja. Az OOP lehetővé teszi, hogy a programot egymással kommunikáló objektumok gyűjteményének tekintsük, nem pedig parancsok és függvények sorozatának.

Az OOP-ban az “objektum” olyan egység, amely adatokat és az adatokkal operáló függvényeket tartalmaz. Az objektumok létrehozása “osztályok” alapján történik, amelyek az objektumok tervrajzaként vagy sablonjaként értelmezhetők. Ha már van egy osztályunk, létrehozhatjuk annak “példányát”, amely egy konkrét, az osztályból készült objektum.

Nézzük meg, hogyan hozhatunk létre egy egyszerű osztályt PHP-ben. Egy osztály definiálásakor az “class” kulcsszót használjuk, amelyet az osztály neve követ, majd a szögletes zárójelek az osztály függvényeit (az úgynevezett “metódusokat”) és az osztály változóit (az úgynevezett “tulajdonságokat” vagy “attribútumokat”) zárják körül:

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

Ebben a példában egy Car nevű osztályt hoztunk létre egy honk nevű függvénnyel (vagy “módszerrel”).

Minden osztály csak egy fő feladatot oldhat meg. Ha egy osztály túl sok mindent csinál, akkor célszerű lehet kisebb, speciális osztályokra osztani.

Az osztályokat általában külön fájlokban tároljuk, hogy a kódot rendszerezve tartsuk és könnyen áttekinthetővé tegyük. A fájlnévnek meg kell egyeznie az osztály nevével, így a Car osztály esetében a fájl neve Car.php lenne.

Az osztályok elnevezésénél célszerű a “PascalCase” konvenciót követni, ami azt jelenti, hogy a névben minden szó nagybetűvel kezdődik, és nincsenek aláhúzások vagy egyéb elválasztójelek. A metódusok és tulajdonságok a “camelCase” konvenciót követik, azaz kisbetűvel kezdődnek.

A PHP egyes metódusai speciális szerepkörrel rendelkeznek, és a __ (két aláhúzás) előtaggal vannak ellátva. Az egyik legfontosabb speciális metódus a “konstruktor”, amelyet a __construct jelöléssel látunk el. A konstruktor egy olyan metódus, amely automatikusan meghívásra kerül, amikor egy osztály új példányát létrehozzuk.

A konstruktort gyakran használjuk egy objektum kezdeti állapotának beállítására. Például egy személyt reprezentáló objektum létrehozásakor a konstruktort használhatjuk a kor, a név vagy más attribútumok beállítására.

Lássuk, hogyan használhatunk konstruktort a PHP-ban:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Outputs: 25

Ebben a példában a Person osztály rendelkezik egy $age tulajdonsággal (változóval) és egy konstruktorral, amely beállítja ezt a tulajdonságot. A howOldAreYou() metódus ezután hozzáférést biztosít a személy életkorához.

A $this álváltozót az osztályon belül az objektum tulajdonságainak és metódusainak elérésére használjuk.

A new kulcsszó egy osztály új példányának létrehozására szolgál. A fenti példában egy új, 25 éves személyt hoztunk létre.

A konstruktor paramétereihez alapértelmezett értékeket is megadhatunk, ha azok nincsenek megadva egy objektum létrehozásakor. Például:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20

Ebben a példában, ha a Person objektum létrehozásakor nem ad meg életkort, az alapértelmezett 20-as értéket fogja használni.

A szép dolog az, hogy a tulajdonság definíciója a konstruktoron keresztül történő inicializálással lerövidíthető és leegyszerűsíthető így:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

A teljesség kedvéért a konstruktorok mellett az objektumok rendelkezhetnek destruktorokkal ( __destruct), amelyeket az objektum memóriából való kikerülése előtt hívunk meg.

Névterek

A névterek lehetővé teszik, hogy az egymással összefüggő osztályokat, függvényeket és konstansokat rendszerezzük és csoportosítsuk, miközben elkerüljük a névkonfliktusokat. Úgy gondolhat rájuk, mint a számítógép mappáira, ahol minden mappa egy adott projekthez vagy témához kapcsolódó fájlokat tartalmaz.

A névterek különösen hasznosak nagyobb projekteknél vagy harmadik féltől származó könyvtárak használatakor, ahol osztályok elnevezési konfliktusok merülhetnek fel.

Képzelje el, hogy van egy Car nevű osztálya a projektjében, és a Transport nevű névtérben szeretné elhelyezni. Ezt a következőképpen tenné:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

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

$car = new Transport\Car;

Az egyszerűsítés érdekében a fájl elején megadhatja, hogy melyik osztályt szeretné használni egy adott névtérből, így a teljes elérési út megadása nélkül hozhat létre példányokat:

use Transport\Car;

$car = new Car;

Öröklés

Az öröklés az objektumorientált programozás egyik eszköze, amely lehetővé teszi új osztályok létrehozását a már létező osztályok alapján, örökölve azok tulajdonságait és metódusait, és szükség szerint kibővítve vagy átdefiniálva azokat. Az öröklés biztosítja a kód újrafelhasználhatóságát és az osztályhierarchiát.

Egyszerűen fogalmazva, ha van egy osztályunk, és szeretnénk létrehozni egy másik, abból származtatott, de némi módosítással rendelkező osztályt, akkor az új osztályt “örökölhetjük” az eredetiből.

A PHP-ben az öröklés a extends kulcsszóval valósul meg.

A mi Person osztályunk az életkori információkat tárolja. Létrehozhatunk egy másik osztályt, a Student, amely a Person kiterjesztése, és hozzáadhatja a tanulmányi területre vonatkozó információkat.

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

class Person
{
	private $age;

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

	function printInformation()
	{
		echo "Age: {$this->age} years\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		parent::printInformation();
		echo "Field of study: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

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

  • A extends kulcsszóval bővítettük a Person osztályt, ami azt jelenti, hogy a Student osztály minden metódust és tulajdonságot a Person osztályból örököl.
  • A parent:: kulcsszó lehetővé teszi számunkra, hogy a szülő osztály metódusait hívjuk. Ebben az esetben a Person osztály konstruktorát hívtuk meg, mielőtt a Student osztályhoz hozzáadtuk volna a saját funkcionalitásunkat. És hasonlóképpen a printInformation() felmenő osztály metódusát, mielőtt a tanulói adatokat listáznánk.

Az öröklés olyan helyzetekre való, amikor az osztályok között “is a” kapcsolat van. Például egy Student egy Person. A macska egy állat. Lehetővé teszi számunkra, hogy olyan esetekben, amikor egy objektumot (pl. “Person”) várunk a kódban, egy származtatott objektumot használjunk helyette (pl. “Student”).

Lényeges felismerni, hogy az öröklés elsődleges célja nem a kódduplikáció megakadályozása. Éppen ellenkezőleg, az öröklés helytelen használata összetett és nehezen karbantartható kódhoz vezethet. Ha nincs “is a” kapcsolat az osztályok között, akkor az öröklés helyett a kompozíciót kell fontolóra vennünk.

Vegyük észre, hogy a Person és a Student osztályok printInformation() metódusai kissé eltérő információkat adnak ki. És hozzáadhatunk más osztályokat is (például Employee), amelyek ennek a metódusnak más megvalósításait biztosítják. A különböző osztályok objektumainak azt a képességét, hogy ugyanarra a metódusra különböző módon reagáljanak, polimorfizmusnak nevezzük:

$people = [
	new Person(30),
	new Student(20, 'Computer Science'),
	new Employee(45, 'Director'),
];

foreach ($people as $person) {
	$person->printInformation();
}

Kompozíció

A kompozíció egy olyan technika, ahol ahelyett, hogy egy másik osztály tulajdonságait és metódusait örökölnénk, egyszerűen csak használjuk annak példányát a saját osztályunkban. Ez lehetővé teszi, hogy több osztály funkcionalitását és tulajdonságait kombináljuk anélkül, hogy bonyolult öröklési struktúrákat hoznánk létre.

Például van egy Engine és egy Car osztályunk. Ahelyett, hogy azt mondanánk, hogy “Egy autó egy motor”, azt mondjuk, hogy “Egy autónak van egy motorja”, ami egy tipikus kompozíciós kapcsolat.

class Engine
{
	function start()
	{
		echo 'Engine is running.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo 'The car is ready to drive!';
	}
}

$car = new Car;
$car->start();

Itt a Car nem rendelkezik a Engine osztály összes tulajdonságával és metódusával, de a $engine tulajdonságon keresztül hozzáférhet azokhoz.

A kompozíció előnye a nagyobb tervezési rugalmasság és a jobb alkalmazkodóképesség a jövőbeli változásokhoz.

Láthatóság

A PHP-ben az osztályok tulajdonságai, metódusai és konstansai számára meghatározható a “láthatóság”. A láthatóság határozza meg, hogy hol lehet hozzáférni ezekhez az elemekhez.

  1. Public: Ha egy elemet public-ként jelölünk, az azt jelenti, hogy bárhonnan, akár az osztályon kívülről is elérhetjük.
  2. Védett: Egy protected jelű elem csak az osztályon belül és annak összes leszármazottján (az osztályból öröklődő osztályok) belül érhető el.
  3. Privát: Ha egy elem private, akkor csak azon az osztályon belül érhető el, ahol definiálták.

Ha nem adja meg a láthatóságot, a PHP automatikusan public értékre állítja.

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

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		echo $this->privateProperty;    // Works
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Folytatva az osztályöröklést:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

Ebben az esetben a printProperties() metódus a ChildClass osztályban hozzáférhet a nyilvános és védett tulajdonságokhoz, de nem férhet hozzá a szülőosztály privát tulajdonságaihoz.

Az adatoknak és a metódusoknak a lehető legrejtettebbnek kell lenniük, és csak egy meghatározott interfészen keresztül lehet őket elérni. Ez lehetővé teszi, hogy az osztály belső implementációját megváltoztassa anélkül, hogy a kód többi részét befolyásolná.

Végleges kulcsszó

A PHP-ben a final kulcsszót használhatjuk, ha meg akarjuk akadályozni, hogy egy osztály, metódus vagy konstans öröklődjön vagy felülíródjon. Ha egy osztályt final jelöli, akkor nem bővíthető. Ha egy metódus final-ként van megjelölve, akkor nem lehet felülírni egy alosztályban.

Ha tudatában vagyunk annak, hogy egy bizonyos osztály vagy metódus nem lesz többé módosítható, könnyebben tudunk változtatásokat végrehajtani anélkül, hogy aggódnánk a lehetséges konfliktusok miatt. Például hozzáadhatunk egy új metódust anélkül, hogy attól kellene tartanunk, hogy egy leszármazottnak már van egy ugyanilyen nevű metódusa, ami ütközéshez vezethet. Vagy megváltoztathatjuk egy metódus paramétereit, szintén anélkül, hogy azt kockáztatnánk, hogy következetlenséget okozunk egy leszármazottban lévő felülhajtott metódussal.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

Ebben a példában a FinalClass végleges osztályból való öröklés megkísérlése hibát eredményez.

Statikus tulajdonságok és metódusok

Amikor egy osztály “statikus” elemeiről beszélünk a PHP-ben, akkor olyan metódusokra és tulajdonságokra gondolunk, amelyek magához az osztályhoz tartoznak, nem pedig az osztály egy adott példányához. Ez azt jelenti, hogy nem kell létrehozni az osztály egy példányát ahhoz, hogy hozzáférjünk 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, nem pedig annak példányaihoz, nem használhatja a $this pszeudováltozót statikus metódusokon belül.

A statikus tulajdonságok használata buktatókkal teli homályos kódhoz vezet, ezért soha ne használd őket, és itt nem is mutatunk példát. A statikus metódusok viszont hasznosak. Íme egy példa:

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

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

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

Ebben a példában létrehoztunk egy Calculator osztályt két statikus metódussal. Ezeket a metódusokat közvetlenül meg tudjuk hívni anélkül, hogy az osztály példányát létrehoznánk a :: operátor segítségével. A statikus metódusok különösen hasznosak olyan műveleteknél, amelyek nem függnek egy adott 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 a program végrehajtása során soha nem változnak. A változókkal ellentétben a konstansok értéke változatlan marad.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): int
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Output: 4

Ebben a példában van egy Car osztályunk a NumberOfWheels konstanssal. Az osztályon belüli konstans elérésekor az osztály neve helyett a self kulcsszót használhatjuk.

Objektum interfészek

Az objektum interfészek az osztályok “szerződéseiként” működnek. Ha egy osztálynak egy objektum interfészt kell implementálnia, akkor tartalmaznia kell az összes metódust, amelyet az interfész definiál. Ez egy nagyszerű módja annak, hogy bizonyos osztályok azonos “szerződést” vagy struktúrát tartsanak be.

A PHP-ben az interfészeket a interface kulcsszóval definiáljuk. Az interfészben definiált összes metódus nyilvános (public). Amikor egy osztály megvalósít egy interfészt, akkor a implements kulcsszót használja.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

Ha egy osztály megvalósít egy interfészt, de nem minden elvárt metódus van definiálva, a PHP hibát fog dobni.

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

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Bark';
	}

	public function guardHouse()
	{
		echo 'Dog diligently guards the house';
	}
}

Absztrakt osztályok

Az absztrakt osztályok más osztályok alapsablonjaiként szolgálnak, de közvetlenül nem hozhatók létre példányaik. Teljes metódusok és olyan absztrakt metódusok keverékét tartalmazzák, amelyeknek nincs meghatározott tartalma. Az absztrakt osztályokból öröklődő osztályoknak meg kell adniuk a szülőktől származó összes absztrakt metódus definícióját.

Az absztrakt osztályok definiálására a abstract kulcsszót használjuk.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

Ebben a példában egy absztrakt osztályunk van egy reguláris és egy absztrakt metódussal. Ezután van egy Child osztályunk, amely a AbstractClass osztályból örököl, és implementációt biztosít az absztrakt metódushoz.

Miben különböznek az interfészek és az absztrakt osztályok? Az absztrakt osztályok absztrakt és konkrét metódusokat is tartalmazhatnak, míg az interfészek csak azt határozzák meg, hogy az osztálynak milyen metódusokat kell implementálnia, de implementációt nem biztosítanak. 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 alapvető fontosságú annak biztosítása, hogy az adatok, amelyekkel dolgozunk, megfelelő típusúak legyenek. A PHP-ben vannak olyan eszközeink, amelyek ezt a biztosítékot nyújtják. Az adatok megfelelő típusának ellenőrzését “típusellenőrzésnek” nevezzük.

Típusok, amelyekkel a PHP-ben találkozhatunk:

  1. Bázis típusok: Ezek közé tartozik a int (egész számok), float (lebegőpontos számok), bool (boolék), string (karakterláncok), array (tömbök) és null.
  2. osztályok: Amikor azt akarjuk, hogy egy érték egy adott osztály példánya legyen.
  3. Interfészek: Meghatározza azon metódusok halmazát, amelyeket egy osztálynak implementálnia kell. Egy interfésznek megfelelő értéknek rendelkeznie kell ezekkel a metódusokkal.
  4. Keverék típusok: Megadhatjuk, hogy egy változónak több megengedett típusa is lehet.
  5. Void: Ez a speciális típus azt jelzi, hogy egy függvény vagy metódus nem ad vissza értéket.

Lássuk, hogyan módosíthatjuk a kódot a típusok felvételére:

class Person
{
	private int $age;

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

	public function printAge(): void
	{
		echo "This person is {$this->age} years old.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

Így biztosítjuk, hogy a kódunk a megfelelő típusú adatokat várja el és a megfelelő típusú adatokkal dolgozik, ami segít megelőzni a lehetséges hibákat.

Néhány típus nem írható közvetlenül PHP-ben. Ebben az esetben a phpDoc kommentben szerepelnek, ami a PHP kód dokumentálásának szabványos formátuma, a /** kezdődik és a */ végződik. Lehetővé teszi, hogy osztályok, metódusok stb. leírását adjuk hozzá. Valamint az összetett típusok felsorolását az úgynevezett annotációk segítségével @var, @param és @return. 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 Registry
{
	/** @var array<Person>  indicates that it's an array of Person objects */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

Összehasonlítás és azonosság

A PHP-ben kétféleképpen lehet objektumokat összehasonlítani:

  1. Érték-összehasonlítás ==: Ellenőrzi, hogy az objektumok ugyanabba az osztályba tartoznak-e, és tulajdonságaikban ugyanazok az értékek szerepelnek-e.
  2. Azonosság ===: Ellenőrzi, hogy az objektum azonos példányáról van-e szó.
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

A instanceof Üzemeltető

A instanceof operátor lehetővé teszi annak meghatározását, hogy egy adott objektum egy adott osztály példánya, leszármazottja, vagy egy bizonyos interfész megvalósítója-e.

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

class Person
{
	private int $age;

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

class Student extends Person
{
	private string $major;

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

$student = new Student(20, 'Computer Science');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

A kimenetekből látható, hogy a $student objektumot a Student és a Person osztályok példányának tekintjük.

Folyékony interfészek

A “folyékony interfész” egy olyan technika az OOP-ban, amely lehetővé teszi a metódusok egyetlen hívással történő láncolását. Ez gyakran egyszerűsíti és egyértelművé teszi a kódot.

A folyékony interfész kulcseleme, hogy a lánc minden egyes metódusa az aktuális objektumra való hivatkozást adja vissza. Ezt a return $this; használatával érjük el a metódus végén. Ez a programozási stílus gyakran társul a “setters” nevű metódusokkal, amelyek az objektum tulajdonságainak értékeit állítják be.

Nézzük meg, hogyan nézhet ki egy folyékony interfész az e-mailek küldéséhez:

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

Ebben a példában a setFrom(), setRecipient() és setMessage() metódusok a megfelelő értékek (feladó, címzett, üzenet tartalma) beállítására szolgálnak. Az egyes értékek beállítása után a metódusok visszaadják az aktuális objektumot ($email), ami lehetővé teszi számunkra, hogy egy másik metódust láncoljunk utána. Végül meghívjuk a send() metódust, amely ténylegesen elküldi az e-mailt.

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

Másolás a clone címen

A PHP-ben a clone operátor segítségével létrehozhatjuk egy objektum másolatát. Így egy új, azonos tartalmú példányt kapunk.

Ha egy objektum másolásakor szükségünk van néhány tulajdonságának módosítására, akkor az osztályban definiálhatunk egy speciális __clone() metódust. Ez a metódus automatikusan meghívódik, amikor az objektumot klónozzuk.

class Sheep
{
	public string $name;

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

	public function __clone()
	{
		$this->name = 'Clone of ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Outputs: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Outputs: Clone of Dolly

Ebben a példában van egy Sheep osztályunk, amelynek egy tulajdonsága a $name. Amikor klónozzuk ennek az osztálynak egy példányát, a __clone() metódus biztosítja, hogy a klónozott bárány neve a “Clone of” előtagot kapja.

A tulajdonságok.

A PHP-ban a Traits egy olyan eszköz, amely lehetővé teszi a metódusok, tulajdonságok és konstansok megosztását az osztályok között, és megakadályozza a kód duplikálását. Úgy gondolhatsz rájuk, mint egy “másolás és beillesztés” mechanizmusra (Ctrl-C és Ctrl-V), ahol egy tulajdonság tartalma “beillesztésre” kerül 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 meg egy egyszerű példát a vonások PHP-ban való használatára:

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Outputs 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

Ebben a példában van egy Honking nevű tulajdonságunk, amely egy metódust tartalmaz: honk(). Ezután van két osztályunk: Car és Truck, amelyek mindkettő a Honking tulajdonságot használja. Ennek eredményeképpen mindkét osztály “rendelkezik” a honk() metódussal, és mindkét osztály objektumain meg tudjuk hívni azt.

A tulajdonságok lehetővé teszik az osztályok közötti egyszerű és hatékony kódmegosztást. Nem lépnek be az öröklési hierarchiába, azaz a $car instanceof Honking a false-t fogja visszaadni.

Kivételek

A kivételek az OOP-ban lehetővé teszik számunkra a hibák és váratlan helyzetek méltóságteljes kezelését a kódunkban. Ezek olyan objektumok, amelyek információt hordoznak egy hibáról vagy szokatlan helyzetről.

A PHP-ben van egy beépített osztályunk, a Exception, amely az összes kivétel alapjául szolgál. Ez több olyan metódussal rendelkezik, 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.

Ha hiba lép fel a kódban, a throw kulcsszóval “dobhatjuk” a kivételt.

function division(float $a, float $b): float
{
	if ($b === 0) {
		throw new Exception('Division by zero!');
	}
	return $a / $b;
}

Ha a division() függvény második argumentumként null értéket kap, akkor a 'Division by zero!' hibaüzenetű kivételt dob. Annak érdekében, hogy a program ne essen össze a kivétel dobásakor, a try/catch blokkban csapdába ejtjük azt:

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

Az olyan kódot, amely kivételt dobhat, egy blokkba csomagoljuk try. Ha a kivételt dobjuk, a kód végrehajtása átkerül a catch blokkba, ahol a kivételt kezelhetjük (pl. hibaüzenetet írhatunk).

A try és a catch blokkok után hozzáadhatunk egy opcionális finally blokkot, amely mindig végrehajtásra kerül, függetlenül attól, hogy a kivételt dobta-e vagy sem (még akkor is, ha a return, break vagy continue blokkot használjuk a try vagy catch blokkban):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// Code that is always executed whether the exception has been thrown or not
}

Saját kivételosztályokat (hierarchiát) is létrehozhatunk, amelyek az Exception osztályból öröklődnek. Példaként tekintsünk egy egyszerű banki alkalmazást, amely lehetővé teszi a befizetéseket és a kifizetéseket:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

Egyetlen try blokkhoz több catch blokk is megadható, ha különböző típusú kivételekre számít.

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

Ebben a példában fontos megjegyezni a catch blokkok sorrendjét. Mivel minden kivétel a BankingException-tól öröklődik, ha ez a blokk lenne az első, akkor minden kivételt elkapnánk benne anélkül, hogy a kód elérné a következő catch blokkokat. Ezért fontos, hogy a specifikusabb (azaz másoktól öröklődő) kivételek a catch blokkok sorrendjében előrébb legyenek, mint a szülő kivételek.

Iterációk

A PHP-ben a foreach ciklus segítségével végighaladhat az objektumokon, hasonlóan a tömbökön való végighaladáshoz. Ahhoz, hogy ez működjön, az objektumnak egy speciális interfészt kell megvalósítania.

Az első lehetőség a Iterator interfész implementálása, amely a current() metódusokkal rendelkezik, amelyek visszaadják az aktuális értéket, a key() metódus visszaadja a kulcsot, a next() metódus a következő értékre lép, a rewind() metódus az elejére lép, és a valid() metódus ellenőrzi, hogy a végén vagyunk-e már.

A másik lehetőség a IteratorAggregate interfész implementálása, amelynek csak egy metódusa van: getIterator(). Ez vagy egy helyőrző objektumot ad vissza, amely a traverzálást biztosítja, vagy lehet egy generátor, ami egy speciális függvény, amely a yield segítségével adja vissza a kulcsokat és az értékeket egymás után:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

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

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Age: {$person->age} years\n";
}

Legjobb gyakorlatok

Ha már az objektumorientált programozás alapelveivel megismerkedtél, elengedhetetlen, hogy az OOP legjobb gyakorlataira koncentrálj. Ezek segítenek olyan kódot írni, amely nemcsak funkcionális, hanem olvasható, érthető és könnyen karbantartható is.

  1. Separation of Concerns: Minden osztálynak egyértelműen meghatározott felelősségi körrel kell rendelkeznie, és csak egy elsődleges feladattal kell foglalkoznia. Ha egy osztály túl sok mindent csinál, célszerű lehet kisebb, specializált osztályokra bontani.
  2. Kapszulázás: Az adatoknak és a metódusoknak a lehető legrejtettebbnek kell lenniük, és csak egy meghatározott interfészen keresztül lehet hozzájuk hozzáférni. Ez lehetővé teszi, hogy egy osztály belső megvalósítását megváltoztassuk anélkül, hogy a kód többi részét befolyásolná.
  3. Függőségi injektálás: Ahelyett, hogy közvetlenül egy osztályon belül hoznánk létre függőségeket, inkább kívülről “injektáljuk” azokat. Ennek az elvnek a mélyebb megértéséhez ajánljuk a Dependency Injection fejezeteket.
verzió: 4.0