PHP class public, private en protected
Wie kan mij uitleggen wat nu het verschil is tussen public, private en protected in een PHP class?
Hoe en wanneer gebruik je welke als je een class gaat programmeren.
gr. Sebastiaan
Lees hier eens: https://torquemag.io/2016/05/understanding-concept-visibility-object-oriented-php/
Gewijzigd op 02/11/2020 13:22:28 door - Ariën -
Via: http://www.expertphp.in/article/visibility-in-php-classes
Gewijzigd op 05/11/2020 09:45:48 door Ward van der Put
Code (php)
Meestal (eigenlijk altijd) wil je dit niet, omdat iedereen nu zomaar alle eigenschappen kan aanpassen. Meestal wil je een bepaalde vorm van controle behouden. Daarom worden vaak 'setters' en 'getters' gebruikt. De eigenschappen, meestal 'properties' genoemd, maak je dan private (of protected als je de class zou extenden, maar daar zal ik je nu niet mee lastigvallen).
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
class Person {
private $name;
public function getName() {
return $this->name;
}
public function setName($name) {
if (empty($this->name)) {
$this->name = $name;
} else {
echo 'Error, name is already set!';
}
}
}
$person = new Person();
$person->setName('Sebastiaan');
echo $person->getName();
// Sebastiaan
$person->setName('Karel');
// Error, name is already set!
?>
class Person {
private $name;
public function getName() {
return $this->name;
}
public function setName($name) {
if (empty($this->name)) {
$this->name = $name;
} else {
echo 'Error, name is already set!';
}
}
}
$person = new Person();
$person->setName('Sebastiaan');
echo $person->getName();
// Sebastiaan
$person->setName('Karel');
// Error, name is already set!
?>
Als ik je mag adviseren, dan raad ik je aan om altijd private te gebruiken (tenzij je classes gaat extenden dan heb je mogelijk protected nodig).
Ozzie PHP op 02/11/2020 14:43:21:
Als ik je mag adviseren, dan raad ik je aan om altijd private te gebruiken (tenzij je classes gaat extenden dan heb je mogelijk protected nodig).
Zou het om die reden daarom eigenlijk niet beter zijn om altijd protected (als default) te gebruiken in plaats van private? Dit omdat private in zekere zin de principes van OOP tegengaat (uitbreidbaarheid).
Ik zou het dus omdraaien: tenzij je expliciete redenen hebt om iets niet te extenden gewoon protected gebruiken, al was het maar om dingen niet op voorhand uit te sluiten.
Thomas van den Heuvel op 02/11/2020 23:00:25:
Zou het om die reden daarom eigenlijk niet beter zijn om altijd protected (als default) te gebruiken in plaats van private? Dit omdat private in zekere zin de principes van OOP tegengaat (uitbreidbaarheid).
Ozzie PHP op 02/11/2020 14:43:21:
Als ik je mag adviseren, dan raad ik je aan om altijd private te gebruiken (tenzij je classes gaat extenden dan heb je mogelijk protected nodig).
Zou het om die reden daarom eigenlijk niet beter zijn om altijd protected (als default) te gebruiken in plaats van private? Dit omdat private in zekere zin de principes van OOP tegengaat (uitbreidbaarheid).
Daar valt iets voor te zeggen. Ik vind het zelf prettig om alles in eerste instantie op private te zetten en pas 'vrij te geven' op het moment dat dat ook echt nodig is, zodat je bewust kunt selecteren wat je wel en niet vrijgeeft. Maar jouw aanpak zou ook kunnen. Het belangrijkste is in ieder geval dat je niet alles (of beter gezegd helemaal niks) op public zet. En of je alles op private of protected zet daar valt voor beide keuzes iets voor te zeggen. Zolang je het maar consequent doet.
Ozzie PHP op 03/11/2020 01:12:09:
En of je alles op private of protected zet daar valt voor beide keuzes iets voor te zeggen.
Nou nee, private druist gewoon in tegen het hergebruiken van code. Dit is niet een kwestie van smaak. Deze code zul je eerst aan moeten passen (private -> protected) om hier alsnog verder mee te kunnen. Waarom zou je dit op voorhand dichtmetselen?
Een extra muur bijzetten kan altijd. Een muur ergens alsnog uitslopen is gewoon extra werk.
Ozzie PHP op 03/11/2020 01:12:09:
Zolang je het maar consequent doet.
Nou nee, niet als dit inhoudt dat je iets consequent verkeerd doet. Je moet een reden hebben om specifiek protected of private (of zelfs public) te gebruiken. Dit is niet enkel syntactische suiker. Het argument voor protected (versus private) is dat je direct kunt extenden. De reden voor private zou kunnen zijn dat je dingen expliciet niet wilt extenden, maar hoe vaak komt dat voor? Heb je daar praktijkvoorbeelden van? En blijkt uit het gebruik van de class niet al vanzelf dat het extenden niet zoveel zin heeft of niet logisch is? Dit hoef je niet expliciet/op voorhand af te dwingen met private, tenzij dit dus echt de bedoeling is maar nogmaals, hoe vaak is dat echt nodig?
Het is beter om dingen open te houden in plaats van dingen achteraf open te breken, het laatste is gewoon meer werk. Dit valt deels onder het kopje "defensief programmeren" waarbij je potentieel overbodig toekomstig werk uit de weg gaat.
Het is ook niet een kwestie van "vrijgeven" want het gebruik is nog steeds beperkt tot binnen de class, of met protected afgeleide classes van zo'n class.
Ik programmeer graag defensief alsin dat ik properties graag beperk tot de eigen class tenzij anders nodig blijkt te zijn. Daarbij moet ik zeggen dat ik al een tijdje niet meer met OOP bezig ben geweest. Ik zal er eens over nadenken of het een goede gewoonte is om voortaan protected te gebruiken.
Wel is dan de vraag wat dan nog het bestaansrecht van private is?
hier staan een aantal interessante punten. Misschien is het interessant om private vs protected toch wat verder te verkennen.
Wat voor keuze je ook maakt (public, protected, private). Deze zou ergens op gebaseerd moeten zijn en een reden moeten hebben.
Dan is er misschien nog een andere overweging: elke keer als je deze afweging maakt, moet je hier tijd aan spenderen. Als je private meeneemt in deze overweging is het antwoord niet altijd even eenduidig. Misschien is het op enig moment onbekend of het handig is om iets later nog te kunnen extenden.
Je zou ook kunnen opteren voor een "simpele beslisboom": indien iets direct opvraagbaar zou moeten zijn via het object: maak het public. In alle andere gevallen maak je het (voorlopig) protected.
En ook: het gebruik van private kan zorgen voor "onvoorspelbaar" gedrag in je code. Indien je een methode extend in de veronderstelling dat deze protected was en kan het even duren voordat je in de gaten hebt dat stiekem toch de parent methode werd gebruikt omdat deze private was. In zekere zin zorgt private ook voor meer complexiteit.
Hm, Wat voor keuze je ook maakt (public, protected, private). Deze zou ergens op gebaseerd moeten zijn en een reden moeten hebben.
Dan is er misschien nog een andere overweging: elke keer als je deze afweging maakt, moet je hier tijd aan spenderen. Als je private meeneemt in deze overweging is het antwoord niet altijd even eenduidig. Misschien is het op enig moment onbekend of het handig is om iets later nog te kunnen extenden.
Je zou ook kunnen opteren voor een "simpele beslisboom": indien iets direct opvraagbaar zou moeten zijn via het object: maak het public. In alle andere gevallen maak je het (voorlopig) protected.
En ook: het gebruik van private kan zorgen voor "onvoorspelbaar" gedrag in je code. Indien je een methode extend in de veronderstelling dat deze protected was en kan het even duren voordat je in de gaten hebt dat stiekem toch de parent methode werd gebruikt omdat deze private was. In zekere zin zorgt private ook voor meer complexiteit.
Gewijzigd op 03/11/2020 16:54:16 door Thomas van den Heuvel
Tja, keuzes ... persoonlijk denk ik dat het nooit verstandig is om een eigenschap public te maken, omdat je er dan geen enkele controle op hebt. Private, protected .. pfff ... mijn insteek komt inderdaad meer overeen met sommige van de reacties in jouw eerste link. Zet alles op private en wijzig het pas als het nodig is. Valt daar iets voor te zeggen? Ja, ik denk het wel. Valt er iets voor jouw insteek te zeggen ... zet het op protected voor het geval je het nodig hebt? Ja daar valt ook iets voor te zeggen. Ik denk dat het uiteindelijk dan toch een persoonlijke voorkeur is. Waar voel je je prettig bij? Plak je alvast een pleister om je vinger omdat je je misschien gaat stoten? Of plak je de pleister als je je gestoten hebt?
Ozzie PHP op 04/11/2020 01:06:12:
persoonlijk denk ik dat het nooit verstandig is om een eigenschap public te maken, omdat je er dan geen enkele controle op hebt.
Dit "probleem" los je op door magic getters en setters te gebruiken ala https://www.yiiframework.com/doc/guide/2.0/en/concept-properties . Iets wat je ooit als een (normale) public var bent begonnen kun je daarna altijd nog via een getter en/of setter "wegstoppen" (incl. controle slag). Zonder dat je je hele codebase door hoeft (of iemand die jouw code gebruikt!) om overal van ->prop ==> ->getProp() te maken (of ->prop = $value ==> ->setProp($value) ).
Ja, ik weet het: overhead. Maar elke keer als je van een functie gebruik maakt is dat een overweging (performance vs efficiency). Het voorkomt in ieder geval dat je op voorhand altijd al een setje getters en setters staat te "boiler platen" omdat je er "misschien ooit nog eens" een getter/setter voor nodig hebt.
Het hangt er vanaf hoe strict je je handhaving wil toepassen.
Als jij het prima vindt dat iemand dit kan doen ...
Dan kun je met public properties werken. Als je er achteraf achterkomt dat je toch een vorm van validatie wil gaan toepassen, hoe weet je dan welke property het betreft in je magic set method?
Code (php)
De invulling van __get() en __set() in BaseObject zorgen er dan voor dat de calls bij getAge() en setAge() uitkomen (zoals ik al zei: overhead).
Voor "gebruikers" van dit object (ook intern) blijft alles dan gelijk.
Code (php)
1
2
3
4
5
6
7
2
3
4
5
6
7
<?php
$person = new Person();
$person->age = 'Rob'; //throws error
$person->name = 30;
?>
$person = new Person();
$person->age = 'Rob'; //throws error
$person->name = 30;
?>
Gewijzigd op 04/11/2020 14:22:01 door Rob Doemaarwat
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class basisobject {
function __set(string $eigenschap, $waarde) : void {
trigger_error(get_class($this) . '->'. $eigenschap . ' is niet gedefineerd');
}
}
class mijnobject extends basisobject {
public $email;
function __construct() {$this->emails = 1;}
}
$object = new mijnobject; // geeft error
?>
class basisobject {
function __set(string $eigenschap, $waarde) : void {
trigger_error(get_class($this) . '->'. $eigenschap . ' is niet gedefineerd');
}
}
class mijnobject extends basisobject {
public $email;
function __construct() {$this->emails = 1;}
}
$object = new mijnobject; // geeft error
?>
EDIT: naar aanleiding van Ward zijn opmerking even de code aangepast dat het goed fout gaat :)
En ik kom er net achter dat de magic setter niet meer nodig is in PHP8, dan krijg je sowieso een waarschuwing bij het benaderen van vanalles wat je niet eer hebt gedefineerd:
https://github.com/php/php-src/blob/php-8.0.0RC3/UPGRADING
Code (php)
Dat geeft sowieso al een fout:
Notice: Undefined variable: emails in ... on line 9
Gewijzigd op 04/11/2020 15:32:34 door Ward van der Put
Rob Doemaarwat op 04/11/2020 14:12:38:
De invulling van __get() en __set() in BaseObject zorgen er dan voor dat de calls bij getAge() en setAge() uitkomen (zoals ik al zei: overhead).
De vraag is inderdaad of je dit moet willen.
Plaatje werkt niet meer? Upload hem anders even bij ImgBB.com.
Dit hele betoog is een kwestie van smaak maar naar mijn mening verdwijnt daarmee het expliciete gedrag van je code en daarmee de leesbaarheid.
Alles in je codebase, maakt niet uit waar, kan middels zo'n getter/setter een object ophalen uit de databse, deze muteren en weer opslaan. Dat hoeft geen probleem te zijn maar het maakt dingen als interne validatie en vervolgacties soms lastig te begrijpen.
Bijvoorbeeld dit stukje (psuedocode), dat kun je overal plakken en het werkt zonder morren:
Code (php)
1
2
3
4
5
2
3
4
5
<?php
$admin = $db->findUserByName('admin');
$admin->setPassword('test123');
$db->save($admin);
?>
$admin = $db->findUserByName('admin');
$admin->setPassword('test123');
$db->save($admin);
?>
Als je het op een meer expliciete manier bouwt word duidelijker wat je wil doen. Dan is ook duidelijk dat daar wat validatie plaatsvind in je model (model is een laag, niet een enkel object).
Code (php)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<?php
$admin = $db->findUserByName('admin');
try {
$admin->changePassword('test123');
$admin->saveChanges();
catch (PasswordException $e) {
// Foutafhandeling
}?>
$admin = $db->findUserByName('admin');
try {
$admin->changePassword('test123');
$admin->saveChanges();
catch (PasswordException $e) {
// Foutafhandeling
}?>
Dat kun je vervolgens zo ver uitbouwen als je wil, bijvoorbeeld met een command/query model maar dat gaat voor de meeste kleine applicaties veel te ver.
Punt is dat als je operaties/mutaties op je model (en eigenlijk alle methods in je codebase) expliciet maakt getters en setters niet nodig zijn en het gedrag je code veel leesbaarder word.
In bovenstaande voorbeelden is het logischer dat er bij changePassword() meer komt kijken dan alleen het setten van een nieuwe waarde, bijvoorbeeld een "your password was changed" mailtje versturen of lengte/complexiteit valideren.
Mocht iemand geïnteresseerd zijn kan ik wel een uitgebreid voorbeeld op GitHub o.i.d. plaatsen
Gewijzigd op 05/11/2020 07:27:43 door Thom nvt
Daar is niets expliciet aan, maar een impliciete bron van lastig te vinden bugs: waar komt nou toch die mail vandaan?
Een soortgelijk probleem introduceer je hier, maar dan omgekeerd: er gebeurt juist te weinig.
Code (php)
1
2
3
4
5
2
3
4
5
<?php
$admin = $db->findUserByName('admin');
$admin->changePassword('test123');
$admin->saveChanges();
?>
$admin = $db->findUserByName('admin');
$admin->changePassword('test123');
$admin->saveChanges();
?>
Kennelijk wijzigt changePassword() het wachtwoord niet, want daarvoor is aansluitend nog een saveChanges() nodig. En die gaat iemand vergeten of op de verkeerde plaats te vroeg of te laat aanroepen.
Eigenlijk is changePassword() dus meer een setter (wanneer we even vergeten en vergeven dat die stiekem ook nog een verborgen dubbele functie als password mailer heeft). Dan is dit juist explicieter:
Ik ben het eens met Ward. Code die uit zichzelf een mailtje verstuurt is zeer onwenselijk. Iemand die de code leest of ermee aan de slag gaat, ziet niet dat er een mail wordt verstuurd. Alleen degene die de betreffende functie changePassword heeft geschreven, is hiervan op de hoogte. Daarnaast moet je ervoor zorgen dat een functie maar 1 ding doet. Moet er nog iets anders gebeuren? Maak dan een nieuwe functie daarvoor.
In plaats van een set of change, kun je ook kiezen voor update. Dan is gelijk duidelijk dat het wachtwoord in de database wordt geüpdatet. Ook is het vreemd hoe jij hier een admin ophaalt en blijkbaar op voorhand op basis van de username al weet dat het een admin is. Is er maar 1 admin in je systeem? Haal die dan op met een aparte functie, of (logischer) gebruik $user als variabelenaam.
Code (php)
1
2
3
4
5
6
7
2
3
4
5
6
7
<?php
$user = new User($id); // $id wordt bepaald aan de hand van gebruikersnaam/wachtwoord
$user->updatePassword('blabla');
$user->mailCredentials();
?>
$user = new User($id); // $id wordt bepaald aan de hand van gebruikersnaam/wachtwoord
$user->updatePassword('blabla');
$user->mailCredentials();
?>
That's it. Hoe duidelijk wil je het hebben.
Ook die try en catch die jij erbij zet zogenaamd voor de duidelijkheid ... die zet je normaliter in je class (eenmalig) of overkoepelend in je framework. Maar waarom zou je die in je normale code flow zetten? Dat moet je dan iedere keer herhalen en dat wil je juist niet. Je moet het jezelf zo makkelijk mogelijk maken, waarvan bovenstaande code een voorbeeld is.
Het is lastig om dit principe goed uit te leggen in één enkele forumpost, er komt nog heel wat meer bij kijken.
Het is gebaseerd op layered architecture/DDD en event-driven applicaties en daar zijn hele boeken over vol geschreven ("DDD in PHP" is een echte aanrader als je met complexe applicaties en business-cases werkt).
Het punt van expliciet maken gaat wel op, het voorbeeld van Ozzie is daarin wat beter neergezet.
Ik probeer de namen van functies en classes altijd zo dicht mogelijk op de realiteit te houden, je code is immers een model (vereenvoudiging) van de werkelijkheid.
In de "echte wereld" stel je bijvoorbeeld niet een wachtwoord in op een persoon. Je wijst hem toe of hij/zij wijzigt hem. "set" komt niet voor en is ambigu. Is het de eerste toewijzing? Een wijziging? Door wie mag dit gedaan worden?
Als je die business verantwoordelijkheid opsplitst in "changePassword" en "assignPassword" kun je bijvoorbeeld verschillende permissie-checks doen, beter een audit-trail bijhouden en, onder bepaalde voorwaarden, makkelijker debuggen.
Zoals gezegd had mijn voorbeel wat beter/uitgebreider kunnen zijn maar ik vind het lastig om dit soort principes kort en bondig uit te leggen.
De exception-handling in de normale code flow is een heel andere discussie die ik nu niet ga aanbreken.
Gewijzigd op 05/11/2020 11:41:44 door Thom nvt