OOP (Object Oriented Programming) 1. Inleiding
2. Overerving (Inheriting)
3. Visibility (public, private of protected)
4. Scope Resulation Operator (::)
5. Namespace
6. Magic Methods/Clone
1. Inleiding
In deze tutorial hoop ik uitleg te geven over hoe met een begin kan maken met het object georiënteerd programmeren (OOP). Ik ga ervan uit dat je al wat basiskennis hebt van PHP en dat je gebruik maakt van PHP 5.3 of hoger.
Objecten zijn “toewijzingen” van classes. Een class is een soort van 'blauwdruk', waarin je de logica van het object begint. Dit klinkt lastig, maar ik zal het uitleggen met een voorbeeld. Ik zal het voorbeeld nemen van een user-module. In deze module gaan we een aantal dingen stoppen. We gaan de mogelijkheid geven om meerdere eigenschappen op te geven en willen de mogelijkheid geven voor een user om iets te zeggen.
In OOP zou dit er zo uitzien:
Class User {
public $name;
public $age;
public function __construct($name,$age) {
$this->name = $name;
$this->age = $age;
}
public function say($message) {
echo $this->name . ': ' . $message . '<br />';
}
public function sayAge() {
$this->say('Ik ben ' . $this->age . ' jaar.');
}
}
$piet = new User('Piet',23);
$klaas = new User('Klaas',26);
$bert = new User('Bert',29);
$piet->say('Bert, alles goed?');
$bert->say('Jawel man!');
$klaas->say('Hoe oud ben jij Piet?');
$piet->sayAge();
?>
Er wordt een Class 'User' aangemaakt. In deze class geef je allerlei mogelijkheden (in dit geval om een naam/leeftijd op te geven, om iets te willekeurigs te zeggen en om je leeftijd te zeggen.
Onder de class maak ik drie objecten aan ($piet, $klaas en $bert). Op het moment dat ik zeg 'new User', weet PHP dat ik een class bedoel en gaat hij op zoek naar de class. Omdat ik achter newUser ('Piet',21) heb gezet gaat hij op zoek naar de __construct method (die is de standaard 'init' functie) en hij verwacht 2 gegevens (naam en leeftijd). Hierdoor komt de class eigenlijk tot leven en worden zijn alle variabelen/functies tot mijn beschikking bij Piet. Daarna doe ik hetzelfde voor de andere personen en uiteindelijk heb ik dus drie gebruikers.
Nu kan ik via het object ($piet, $berg en $klaas) de methodes aanroepen en ook de variabelen. Als ik $piet->say('iets'); opgeef betekent dit dat ik de 'say' method aanroep van het object piet. Piet gaat hierdoor praten en je ziet zijn naam staan, omdat het object de naam heeft gekregen bij het initialiseren (__construct).
Ik zei ook dat je eventueel de variabelen direct aan zou kunnen roepen. Dit kan door het object aan te roepen en de variabelenaam erachter. Als ik de leeftijd van piet wil weten, kan ik deze zo ophalen:
$piet->age;
Bij de methode 'sayAge' zien we iets anders wat heel belangrijk is, en dat is een speciale variabele '$this'. In deze variabele zitten alle variabelen en methodes van het object. Dus als je in het object $piet zit en je gebruikt $this, zit daar alles in van het object piet. Dus je zou kunnen doen $this->say(), $this->name, $this->age. Dit geeft je dus de mogelijkheid om vanuit één method, andere methods aan te roepen die binnen deze class zitten.
Voor ingevulde variabelen/argumenten
Het is de mogelijkheid (net als bij normale functies) om voor ingevulde variabelen op te geven bij de methods. Deze hoef je dan niet verplicht mee te geven als je de method aanroept.
public function setGender($gender = 'M') {
$this->gender = $gender;
}
?>
Als ik in dit voorbeeld $piet->setGender(); zou opgeven, dan wordt zijn gender (geslacht) op 'M' gezet.
2. Overerving (Inheriting)
Inheritance is een belangrijke eigenschap van OOP. Ik wil laten zien wat overerving is, en wil beginnen met een voorbeeld van ons bovenstaande voorbeeld. De class die we gemaakt hadden was 'User'. Om te laten zien hoe het werkt, wil ik daar nu een class bij maken genaamd “Worker”.
Class User {
public $name;
public $age;
public function __construct($name,$age) {
$this->name = $name;
$this->age = $age;
}
public function say($message) {
echo $this->name . ': ' . $message . '<br />';
}
public function sayAge() {
$this->say('Ik ben ' . $this->age . ' jaar.');
}
}
Class Worker extends User {
public $salery;
public function saySalery() {
$this->say('Ik verdien ' . $this->salery . ' euro per maand.');
}
}
$piet = new Worker('Piet',23);
$piet->salery = 3000;
$piet->say('Ik ben Piet.');
$piet->sayAge();
$piet->saySalery();
?>
We hebben nu 2 classes. Met Users kan ik gebruikers aanmaken die een naam en een leeftijd hebben. Ook heb ik de class 'Worker' aangemaakt die de class 'User' extend. Hiermee heb ik alle eigenschappen die in User zitten ook tot mijn beschikking in Worker. In het voorbeeld zie je dat ik piet nu als worker aanmaakt. Ik geef aan dat zijn salaris 3000 euro per maand is. Daarna zie je dat ik dingen kan zeggen (wat een eigenschap van 'User' is) en zowel eigen methods als die van de User-class kan gebruiken. In de method kan ik ook $this gebruiken wat zowel alle eigenschappen van User als van Worker heeft.
Een ander aspect van overerving is dat je functies kunt herschrijven. We hebben in User de method 'sayAge'. Stel dat ik deze wil herschrijven om (in dit geval) een andere zin te geven. Dan zou de class Worker er zo uit zien:
Class Worker extends User {
public $salery;
public function saySalery() {
$this->say('Ik verdien ' . $this->salery . ' euro per maand.');
}
public function sayAge($age) {
$this->say('Als medewerker ben ik ' . $age . ' jaar.');
}
}
?>
Omdat ik dezelfde naam gebruik (sayAge) wordt deze overschreven. Ik geef een argument mee (terwijl dit bij User niet het geval is). Hierdoor moet ik deze functie nu zo aanroepen:
$piet->sayAge(30);
3. Visibility (public, private of protected)
Nu willen we naar een ander onderdeel gaan en dat is hoe we bepaalde beveiligingen aan kunnen brengen. In ons eerste voorbeeld zien we dat zowel de methods/functies als de variabelen public zijn. Dit betekent dat je ze altijd kan benaderen zowel binnen de class zelf, als buiten de class. Naast public is er ook protected. Als een method/variabele protected is, dan kan deze alleen aangeroepen worden binnen de eigen class of binnen de classes die deze class extenden. Private gaat nog een stap verder en zorgt ervoor dat je deze method/variabele alleen kunt gebruiken binnen je eigen class. Als je bijvoorbeeld een private method probeert aan te roepen buiten de class dan krijg je de error: Fatal error: Call to private method {method}.
Om duidelijk te zien hoe je public/private/protected aangeeft, hieronder een voorbeeldje.
Class User {
// vrij aanroepbaar
public $var1;
public function method1() {
}
// alleen in eigen class of classes die deze class extenden aanroepbaar
protected $var2;
protected function method2() {
}
// alleen in eigen class aanroepbaar
private $var3;
private function method3() {
}
}
?>
4. Scope Resulation Operator (::)
Om een object aan te roepen (bv met $this) wordt er gebruikt gemaakt van een -> teken. Hiermee wordt aangegeven dat je eigenschappen wilt benaderen van de variabele (object). Als je werkt met classes heb je ook de mogelijkheid om gebruik te maken van het :: (double colon) teken. Dit teken kun je gebruiken bij static methods/variabelen, overschreven methods (vanuit de parent-class) en constanten. Ik wil deze principes hieronder verder uitwerken met voorbeelden.
Het kan soms voorkomen dat je een method van de parent wilt gebruiken, maar hier nog wat wil toevoegen vanuit de huidige class. Hiervoor is er de constante 'parent'.
Als we de Worker-class die we eerder hebben gebruikt er weer bij pakken dan kunnen we de 'sayAge' method bijvoorbeeld zo uitbreiden:
public function sayAge($age) {
parent::sayAge();
$this->say('Als medewerker ben ik ' . $age . ' jaar.');
}
?>
Parent geeft dus aan dat je deze method wil gebruiken vanuit de parent class (de class die geëxtend wordt).
Naast Parent heb je ook self en static die je kunt gebruiken bij het werken met classes. Het voordeel hiervan is dat je geen object hoeft te instansialiseren, of met andere woorden, je hoeft geen object in het geheugen weg te schrijven. Om dit uit te leggen wil ik opnieuw een voorbeeld gebruiken.
We hebben nog steeds de Worker-class waar we mee werken die de User-class extend. Nu wil ik een method maken die een hond laat blaffen. Deze method ziet er zo uit:
public static function dogBark() {
echo 'Woef! Woef!<br />';
}
?>
Wat we hier zien is dat ik deze method de 'property' static meegeef. Dit betekent dat dit een statische method is. Deze method kun je binnen de class aanroepen door: Self::dogBark() of static::dogBark() toe te voegen. Ook is het mogelijk om deze static method buiten de class aan te roepen door {classname}::dogBark() neer te zetten en dus in dit geval door Worker::dogBark() uit te voeren. Als we deze method uitvoeren binnen of buiten de method dan gaat de hond blaffen. (voorbeeld komt zo meteen).
Ook kun je werken met static variabelen wat iets anders werkt dan een normale variabelen in classes. In ons voorbeeld gaan we een teller maken die telt hoeveel workers er zijn aangemeld. Om dit voor elkaar te krijgen maken we in User een static variabele met de naam 'count'.
public static $count = 0;
Hier geven we dus door het keyword 'static' voor de variabele naam te zetten (bij het initialiseren van de variabele) dat dit een static variabele wordt. In de construct (die elke keer wordt aangeroepen als er een object aangemaakt wordt - en dus als er een User aangemaakt wordt) zetten we een teller op deze manier:
static::$count++;
We gebruiken hier static, maar we zouden ook self kunnen gebruiken. Nu kunnen we nadat de Workers zijn aangemaakt het aantal ophalen via deze code:
echo Worker::$count;
Nu ziet onze class er zo uit:
Class User {
public $name;
public $age;
public static $count = 0; // !
public function __construct($name,$age) {
static::$count++; // !
$this->name = $name;
$this->age = $age;
}
public function say($message) {
echo $this->name . ': ' . $message . '<br />';
}
public function sayAge() {
$this->say('Ik ben ' . $this->age . ' jaar.');
}
}
Class Worker extends User {
public $salery;
public function saySalery() {
$this->say('Ik verdien ' . $this->salery . ' euro per maand.');
}
public function sayAge($age) {
parent::sayAge();
$this->say('Als medewerker ben ik ' . $age . ' jaar.');
}
public static function dogBark() {
echo 'Woef! Woef!<br />';
}
}
$piet = new Worker('Piet',23);
$piet->salery = 3000;
$piet->say('Ik ben Piet.');
$piet->sayAge(10);
$piet->saySalery();
Worker::dogBark();
$klaas = new Worker('Klaas',22);
$klaas->sayAge(23);
$bert = new Worker('Bert',30);
echo Worker::$count; // !
?>
Het verschil tussen self en static is dat self alleen de methods/variabelen van de eigen class tot z'n beschikking heeft, terwijl static alle classes waarmee de huidige class verbonden is tot z'n beschikking heeft.
Een laatste mogelijkheid in classes met de double colon (::) is om contstanten te gebruiken.
Class Test {
const VALUE = 'Dit is een waarde';
}
echo Test::VALUE;
?>
Dit werkt hetzelfde als een static variabele, alleen is het nu een constant waarde. Een constant in een class kan – net als een constant buiten een class – niet meer veranderd worden.
5. Namespace
Vanaf de komst van PHP 5.3 is er ook de mogelijkheid om namespaces te gebruiken. Als je een groot project aan het maken ben is het mogelijk dat je soms dezelfde benamingen wilt gebruiken voor verschillende class. Bijvoorbeeld: je hebt een module voor artikelen die bestaat uit een Category-class en een Article-class. Daarnaast heb je een forum-module waar ook een Category-class voor is en een Item-class. In beide zit een Category-class en dit levert problemen op in PHP. Want als je de Category-class gaat gebruiken, welke moet hij dan gebruiken? Hier komen namespaces van pas. Wat we moeten weten is dat je maar één namespace kunt gebruiken per script (php-bestand) en dat deze bovenaan het script moet staan.
Als je Forums.php hebt en Articles.php en beide worden geïnclude. Forums.php ziet er zo uit:
namespace Forums;
Class Category {
public static function test() {
echo 'Categorie van forums!<br />';
}
}
?>
Articles.php ziet er zo uit:
namespace Articles;
Class Category {
public static function test() {
echo 'Categorie van artikelen!<br />';
}
}
?>
Dan kun je deze in een hoofdbestand (bv index.php) zo includen en aanroepen:
include('Forums.php');
include('Articles.php');
echo Forums\Category::test();
echo Articles\Category::test();
?>
Het is ook mogelijk om een namespace uit te breiden.
Namespace Dev\Test\Forum;
// rest van de code
?>
Als je deze code hebt, dan heb je als het ware een namespace, in een namespace, in een namespace. Misschien vind je dit onhandig, maar als je een routing-class (om te bepalen welke bestanden aangeroepen moeten worden) hebt, kan dit erg handig zijn, omdat je hiermee de locatie van de paden kunt bepalen. Zo heb ik bijvoorbeeld in mijn framework de mogelijkheid om zo een method in een Library te benaderen: Dev\Library\DB\Connector::connect(). Als ik deze code uitvoer zoekt de routing-class in de map Dev/Library/DB naar het bestand Connector.php. In dit bestand staat de namespace 'Dev\Library\DB\Connector' met de class 'Connector'. Hierin zit de static method 'connect'. Dit is slechts één van de voordelen van namespaces, er zijn er nog meer te verzinnen.
Ik moet hier nog één ding aan toevoegen: het gebruik van 'use'.
In de class Forum maak je bijvoorbeeld gebruik van de classes 'Dev\Library\DB\Connector', 'Dev\Modules\Accounts\User' en 'Dev\Vendor\Date'. Als je in elke method deze volledige classnamen (met namespaces) moet gaan invullen wordt je code een stuk groter en onoverzichtelijker. Om dit op te lossen zit er in PHP 5.3 de mogelijkheid om 'use' te gebruiken.
Dit ziet er dan bijvoorbeeld zo uit:
namespace Dev\Modules\Forum;
Use \Dev\Modules\Accounts\User as Account;
Use \Dev\Vendor\Date;
Use Models\Categories;
Class Forum {
Public static function getCategories() {
foreach (Categories::getAll() AS $v) {
echo 'Gebruiker: ' . Account::get($v['id']) . '<br />';
echo 'Datum: ' . Date::format($v['date'],'d-m-Y') . '<br />';
}
}
}
?>
Dit is slechts een voorbeeld, in het echt zal je structuur er waarschijnlijk wat anders uit zien.
Ik wil hier even per regel doorheen:
namespace Dev\Modules\Forum;
Hiermee maak je de namespace dus aan.
Use \Dev\Modules\Accounts\User as Account;
Door de backslash geef je aan dat deze namespace buiten de huidige namespace zit en dat je de hele namespace-naam gaat opgeven van de class die je wilt selecteren. Door 'as' te gebruiken geef je aan dat je die class 'User' (van de betreffende namespace) met de naam 'Account' wil gebruiken.
Use \Dev\Vendor\Date;
Dit is hetzelfde als de voorgaande, alleen zonder de 'as' optie. Hierdoor gebruik je de naam 'Date' om de betreffende class aan te geven.
Use Models\Categories;
Hier staat geen backslash en dit betekent dat deze namespace (Models) in de huidige namespace zit, dus de volledige naam zou zijn: Dev\Modules\Forum\Models\Categories. Ook hier gebruik je de naam van de class om de class te bereiken.
Dan sla ik wat regels over enkom ik bij de foreachlus.
foreach (Categories::getAll() AS $v) {
Hier roep ik de class 'Categories' aan (volledige naam met namespaces: Dev\Modules\Forum\Models\Categories. Hier komen resultaten uit waar ik doorheen wil lussen.
echo 'Gebruiker: ' . Account::get($v['id']) . '<br />';
Hier roep ik de alias Account aan (Dat is de class 'User' met de volledige naam van de namespace: Dev\Modules\Accounts\User) en voer de method 'get' uit.
echo 'Datum: ' . Date::format($v['date'],'d-m-Y') . '<br />';
Hier roep ik de date-class aan (volledige naam met namespace: DevVendorDate).
Op deze manier werken kan dus veel tijd schelen en maakt je code ook wat duidelijker.
6. Magic Methods/Clone
Bij PHP 5 komen ook handige 'magic' methods, dat zijn ingebakken methods om voor jou het programmeren wat makkelijker te maken. Ik zal ze bespreken en een voorbeeld van elk geven.
__construct()
Class Test {
public function __construct() {
echo 'Object aanroep!<br />';
}
}
$n = new Test();
?>
De eerste 'magic' method die ik wil noemen is __construct. We hebben deze eerder al gebruikt, maar omdat dit wel een magic method is, noem ik hem hier ook. Deze method wordt als eerste aangeroepen als je een object 'maakt' (met $var = new Classname();).
__destruct()
Class Test {
public function say($message) {
echo $message . '<br />';
}
public function __destruct() {
echo 'Object stoppen!<br />';
}
}
$n = new Test();
$n->say('Dit is een test die voor de __destruct wordt aangeroepen.');
?>
Deze method is een tegenhanger van __construct(). __construct wordt aangeroepen waarin je een class initialiseert. __destruct wordt aangeroepen wanneer je klaar bent met het gebruiken van de class.
__invoke()
Class Test {
public function __invoke() {
echo 'Dit is een object, niet een method!<br />';
}
}
$t = new Test();
echo $t();
?>
De invoke-method wordt uitgevoerd op het moment dat je een object aanroept als functie. Hierdoor kun je fouten afvangen, of een bepaalde functionaliteit toevoegen die je tijdens het programmeren snel tot je beschikking wilt hebben.
__clone()
Class Test {
public function __clone() {
echo 'Wil je stiekem dit object gaan klonen?<br />';
}
}
$t = new Test();
$t2 = clone $t;
?>
De clone-method wordt aangeroepen wanneer je een object gaat klonen. Deze method wordt eerst aangeroepen, en dan vind het klonen daadwerkelijk plaats.
__call($name,$args)
Class Test {
public function __call($name,$args) {
echo 'Method: ' . $name . '<br />';
echo '<pre>';
print_r($args);
echo '</pre>';
}
}
$t = new Test();
$t->say('hoi');
?>
De call-method wordt aangeroepen als een method wordt aangeroepen die niet bestaat. De call-method geeft 2 argumenten terug. In het eerste argument wordt de naam van de method die je probeerte uit te voeren neergezet. In het tweede argument de bijbehorende argumenten die meegegeven werden. Als deze method bestaat wordt er geen error gegeven dat de method die werd aangeroepen niet bestaat.
__callStatic($name,$args)
Class Test {
public static function __callStatic($name,$args) {
echo 'Method: ' . $name . '<br />';
echo '<pre>';
print_r($args);
echo '</pre>';
}
}
Test::say('hoi');
?>
De callStatic-method wordt aangeroepen als een method die je static wilde aanroepen niet bestaat. Dezelfde regels gelden voor deze method als voor __call() met als verschil dat deze method alleen geldt voor static aanroepen.
__get() / __set()
Class Test {
private $columns = array();
public function __set($key,$value) {
$this->columns[$key] = $value;
}
public function __get($key) {
return $key . ': ' . $this->columns[$key];
}
}
$t = new Test;
$t->title = 'Testtitel';
echo $t->title;
?>
De volgende 2 magic methods zijn __get() en __set(). Deze methods worden aangeroepen als je een variabele declareert of uitprint. In dit geval wordt bij het declareren (maken) van de variabele 'title' er geen variabele aangemaakt in het object zelf, maar in een array binnen het object. Bij het ophalen wordt de method __get() aangeroepen en die geeft de gegevens weer terug. __get en __set zijn vooral handig als je bijvoorbeeld een opslag-functionaliteit wil toevoegen (bijvoorbeeld bij een database class). Stel je hebt een save-method om in één keer de gemaakte array op te slaan, dan hoef je alleen maar de array uit te lezen. Als je dan de database-class extend (in bijvoorbeeld een model), heb je nog steeds die save-method tot je beschikking, ook al gebruik je andere waardes. Dit klinkt misschien wat technisch, vandaar een klein voorbeeldje.
Class MySQL {
private $columns = array();
protected $table;
public function __set($key,$value) {
if (!isset($this->$key)) {
$this->columns[$key] = $value;
}
}
public function __get($key) {
if (!isset($this->$key)) {
return $key . ': ' . $this->columns[$key];
}
}
public function save($id) {
// save $this->columns bij de table
echo 'Table: ' . $this->table . '<br />';
foreach ($this->columns AS $column=>$value) {
echo $column . ' -> ' . $value . '<br />';
}
}
}
Class Articles extends MySQL {
public function __construct() {
$this->table = 'articles';
}
// hier eigen methods voor articles
}
$t = new Articles;
$t->title = 'Testtitel';
$t->active = 1;
$t->save(123); // save bij id 123
?>
Hier zie je dus dat ik een model 'articles' heb die ik aanroep. In die model zitten de methods van dit specifieke model. Ik geef hier aan welke tabel ik gebruik. Nu kan ik waardes ingeven die bij deze tabel horen (dit zou je ook uit de database kunnen halen), en dan save ik deze. Dit moet je verder uitwerken uiteraard, maar dit kan de basis zijn van een database-class.
__unset()
Class Test {
public function __unset($key) {
echo 'verwijder ' . $key . '<br />';
}
}
$t = new Test;
$t->test = 'hoi';
echo $t->test . '<br />';
unset($t->test);
echo $t->test . '<br />'; // dit geeft een error
?>
De unset-method wordt aangeroepen als je de method 'unset' aanroept om een variabele van de class te verwijderen (unsetten) en als je de __get en __set methods gebruikt. In ons voorbeeld bij __set() en __get() kun je dus de waardes van de array unsetten.
__isset($key)
Class Test {
public function __isset($key) {
return isset($this->columns[$key];
}
}
$t = new Test;
if (isSet($t->test)) {
echo 'test bestaat!<br />';
} else {
echo 'test bestaat niet!<br />';
}
?>
De isset-method wordt aangeroepen als je de method 'isset' aanroept om een variabele van de class te controleren (of hij bestaat/gezet is) en als je de __get en __set methods gebruikt. In ons voorbeeld bij __set() en __get() kun je controleren of de waardes in de array staan.
__tostring()
Class Test {
public function __tostring() {
return json_encode($this);
}
}
$t = new Test;
$t->title = 'Titel';
$t->waarde = 'Waarde';
echo $t;
?>
In dit voorbeeld zie je dat de __tostring()-method wordt aangeroepen op het moment dat je een object als string behandelt. In deze method printen wij de inhoud van het object uit als json. Je zou hier ook een controle kunnen maken van alle variabelen en methods die in je class zitten, zodat - als je aan het programmeren bent - je snel kunt controleren welke waardes/methods er in je class zitten.
Er zijn nog twee dingen die ik (waarschijnlijk) in de toekomst nog in deze tutorial wil toevoegen: Interfaces en Traits. Omdat deze twee niet noodzakelijk zijn en ook vrij weinig gebruikt worden in PHP (op dit moment), zal ik deze later toevoegen.
|