PHP class public, private en protected
Thom nvt op 05/11/2020 11:38:43:
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 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).
Dit voorstel om de toegang te kunnen regelen op het niveau van namespaces is daarom dan ook wel aardig.
Martin Fowler heeft wel eens betoogd dat public versus published (PDF) een belangrijker onderscheid is dan public versus private. Hoewel dat artikel over interfaces gaat, noemt het herkenbare aanknopingspunten die evengoed opgaan voor eigenschappen en methoden.
Thom nvt op 05/11/2020 11:38:43:
Het punt van expliciet maken gaat wel op, het voorbeeld van Ozzie is daarin wat beter neergezet.
Dank u ;)
Thom nvt op 05/11/2020 11:38:43:
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.
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.
Op zich interessant. Er is natuurlijk heel veel theorie te vinden en daar staan ongetwijfeld ook slimme waarheden in. Wat ik zelf heb gemerkt, is dat het goed is om dingen uit te proberen in de praktijk (al doende leert men wat wel en niet fijn werkt), maar om vooral ook consequent te zijn en een benadering te vinden die (vrijwel) altijd opgaat.
Even wat dieper ingaand op jouw opmerking. Set is ambigu, wat doet het, wie mag het enz.
Jij pleit voor changePassword en assignPassword.
Persoonlijk denk ik dat je dan een stap te ver gaat.
Ik probeer te werken met bepaalde begrippen waaraan ik me altijd vasthoud en kies dan wat het beste past. Dan kun je denken aan combinaties als get/set of load en save, en eventueel update. Dit zijn begrippen die redelijk duidelijk zijn en vrijwel in alle situaties toepasbaar. Stel ik zou met jouw code moeten werken en jij hebt een user class gemaakt. Via die user class wil ik een wachtwoord instellen (setten).
Hé, setPassword bestaat niet? Nu moet ik de class induiken om te kijken welke functie ik dan wel moet hebben. Aha ... changePassword! Toch? Of, wacht even ... er is ook nog een assignPassword? Huh? Welke moet ik nu hebben? Dit is niet duidelijk.
Je hebt 1 functie waarmee je iets instelt. Wie dat doet maakt niet uit. De functie voert gewoon een taak uit. Betreft dat instellen een wijziging in de database, dan kun je spreken van update in plaats van set.
Het zou nogal omslachtig zijn om voor iedere property een set en een change functie te maken. Dan krijg je dus een setName en een changeName, een setAge en een changeAge. Dat slaat natuurlijk nergens op. Houd de dingen eenvoudig en gebruik (in jouw voorbeeld) set of update, maar ga niet met 2 vage begrippen als change en assign werken.
Bij change en assign ga je uit van de gebruiker (wie roept de code aan) om precies hetzelfde te bewerkstelligen. Als het systeem automatisch een wachtwoord toekent zou je dan assign moeten gebruiken, en als de gebruiker dat doet dan zou je change moeten gebruiken? Lekker verwarrend. Heeft geen meerwaarde.
Zoiets als dit is wél logisch:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// systeem kent automatisch een wachtwoord toe
$pass = $security->generatePassword();
$user->setPassword($pass);
// gebruiker wijzigt z'n wachtwoord
$pass = $security->validatePassword($_POST['pass']);
$user->setPassword($pass);
?>
// systeem kent automatisch een wachtwoord toe
$pass = $security->generatePassword();
$user->setPassword($pass);
// gebruiker wijzigt z'n wachtwoord
$pass = $security->validatePassword($_POST['pass']);
$user->setPassword($pass);
?>
Nu gebruik je één en dezelfde functie. Het verschil is dat het systeem zelf een veilig wachtwoord genereert, en dat een aparte functie het wachtwoord van de gebruiker controleert in geval van een wijziging. Maar in beide gevallen gebruik je setPassword en niet vage omschrijvingen als changePassword en assignPassword. Eén en dezelfde functie doet dus in beide gevallen hetzelfde en dat is ook hoe het hoort te zijn. Rechten afvangen doe je op een ander niveau en heeft niks met de functie zelf te maken.
Dit is dus het punt waar je uitkomt op persoonlijke voorkeur, smaak, doel en omgeving van de applicatie, etc.
Jouw punt is wat mij betreft ook valide, soms is een setter gewoon toepasselijker. Op eenzelfde manier is een simpele CRUD-applicatie soms/vaak gewoon beter dan een even-driven applicatie.
De manier die ik uitlegde komt vooral voort uit enterprise-niveau applicaties en het is dus lang niet altijd toepasselijk maar ik merk dat het expliciet definieren en opslitsen een hoop code ten goede komt. De mate waarin je opsplitst is discutabel en valt i.m.h.o. buiten deze discussie.
Ok, lange inleiding. Here it goes:
Helaas bieden voorbeelden niet altijd een compleet beeld waardoor de discussie staakt op de gegeven voorbeelden, dat wil ik voorkomen.
Ja, je krijgt verschillende methods voor verschillende acties (want change en assign zijn verschillende acties) maar die zitten niet op het niveau van je database representatie (DTO's). De voorbeelden zijn misschien wat te summier om recht te doen aan de opzet maar ik doe nog een poging.
Zo kun je jouw voorbeeld ook anders schrijven:
Code (php)
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<?php
// Genereer een wachtwoord
$passwordGenerator = $registry->get('password_generator'); // Kan natuurlijk ook een ServiceLocator zijn, dat staat los van deze discussie
$user->generatePassword($passwordGenerator);
// Wijzig wachtwoord
$passwordGenerator = $registry->get('password_generator');
$user->changePassword($passwordGenerator, $_POST['pass']); // Of setPassword. En natuurlijk niet zomaar de POST var doorsturen.
?>
// Genereer een wachtwoord
$passwordGenerator = $registry->get('password_generator'); // Kan natuurlijk ook een ServiceLocator zijn, dat staat los van deze discussie
$user->generatePassword($passwordGenerator);
// Wijzig wachtwoord
$passwordGenerator = $registry->get('password_generator');
$user->changePassword($passwordGenerator, $_POST['pass']); // Of setPassword. En natuurlijk niet zomaar de POST var doorsturen.
?>
Dit kun je nog veel uitgebreider en abstracter maken met factories, services, etc. maar om het kort te houden heb ik dat er niet in gezet.
Zowel jouw voorbeeld als deze zijn naar mijn mening correct maar hier is het gebruik van verschillende methods duidelijker en naar mijn mening beter te verantwoorden.
De verantwoordelijkheid van het hashen ligt nu bij het model, niet bij de controller. Het niveau van afdwingen ligt dieper in je applicatie. De User class accepteert natuurlijk een implementatie van een PasswordGeneratorInterface of, nog beter, een EncryptionServiceInterface die de PWGenerator omvat.
Voor wat betreft gebruik van andermans code:
Als jij mijn User-class zou moeten gebruiken dan behoor je daar de interface-documentatie bij te krijgen. Dan zie je in één oogopslag hoe het gebruikt moet worden en heb je dus niet het "hè? setPassword bestaat niet" probleem.
En natuurlijk moet je niet voor elke property een set en change method maken, beperk je model tot de functionaliteit die je nodig hebt en niet meer (YAGNI). Niet elke class of property heeft een set() en update() nodig, zelfs get() is niet altijd nodig.
Als laatste:
Ik vind dat je rechten prima kan en vaak zelfs moet afvangen binnen je (domain) model, al is de manier waarop wat anders dan met een RBAC/ACL implementatie.
Dat neemt dus ook niet weg dat je in je controller-laag ook moet valideren op rechten.
Dit is vooral omdat ik van mening ben dat het model qua gedrag volledig los moet staan van de controller-laag. Oftewel: Alle business-logic moet in je model zitten, niet in je controller. Je business-logic bepaalt dus ook of een bepaalde rol binnen het systeem iets wel of niet mag. (mooi stukje proza over het gebruik van "user" in code: https://codewithoutrules.com/2018/09/21/users-considered-harmful/)
Zoals gezegd: voorbeelden zijn niet altijd compleet of representatief, ook deze niet. Ik zal eens gaan graven naar een concreet stukje code wat het beter weergeeft.
In de tussentijd kan ik je van harte "DDD in PHP" aanraden, kost 25 euro op leanpub en het is echt de moeite waard. Mijn gedachtegang in deze word daar goed in uitgelegd.
Gewijzigd op 05/11/2020 14:48:28 door Thom nvt
In het voorbeeld dat je nu geeft, gaat het inderdaad om 2 verschillende dingen. Een functie die een wachtwoord genereert en een functie die een wachtwoord instelt. Hier heb je dus niet meer het eerdere probleem van change en assign. Dus wat je nu als voorbeeld geeft kan wel ... met als kanttekening dat ik een user class die zelf een wachtwoord genereert discutabel vind. Ik zou denken dat je een aparte class hebt die het wachtwoord genereert en dat je de output daarvan doorstuurt naar de user class. Ik weet niet zozeer wat het voordeel zou zijn om de wachtwoord class door te geven aan een functie binnen de user class. Ik zie de user class meer als een representatie van een gebruiker waarin je zaken kunt opslaan en ophalen.
Meer zoiets, voortbordurend op jouw voorbeeld.
Code (php)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<?php
$services = Services::get();
$password = $services->get('pass_generator');
$password = $password->generate();
$user->setPassword($password);
?>
$services = Services::get();
$password = $services->get('pass_generator');
$password = $password->generate();
$user->setPassword($password);
?>
Je hebt nu een aparte class die op zichzelf een wachtwoord genereert. En je hebt een user class waar je netjes het wachtwoord aan doorgeeft. Op deze manier kun je ieder wachtwoord in die user class stoppen dat je maar wilt. Alles is netjes gescheiden en ook super simpel en clean.
Even heel erg fictief ...
Code (php)
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<?php
if ($subscribed_through_facebook) {
$facebook = getFacebookAPI();
$facebook_user = $facebook->getUser($user_id);
$password = $facebook_user->getPassword();
}
$user->setPassword($password);
?>
if ($subscribed_through_facebook) {
$facebook = getFacebookAPI();
$facebook_user = $facebook->getUser($user_id);
$password = $facebook_user->getPassword();
}
$user->setPassword($password);
?>
Hier haal je dus een wachtwoord van een externe partij vandaan, en die kun je zonder problemen doorgeven aan je user class.
Wat betreft jouw kanttekening: Dat snap ik maar ik denk dat het komt omdat je de User class ook zo ziet: als class.
Denk er meer over na als een entiteit binnen je model. Het is de verantwoordelijkheid van die entiteit om zichzelf geldig te houden. Daar kun je hem de tools voor aanreiken (cryptoService in deze) maar hij moet het zelf doen omdat hij de enige is die weet wat geldig is en niet (open/closed en dependency inversion principes).
De class met de naam "User" is slechts een klein onderdeel van de entiteit/het model.
Zoals ik eerder zei, "Model" is een laag, niet een bak met getters/setters om met je database uit te wisselen, dat is namelijk een DTO.
Voor ik zelf een uitgebreid voorbeeld in elkaar ga kloppen; ik bedacht mij gisteren dat alle code uit dat boek op GitHub staat: https://github.com/dddinphp
Een mooi voorbeeld van zo'n ontwerp staat in de "LastWishes" applicatie (Symfony Silex backend en Angular frontend)
Het User model daarvan staat hier: https://github.com/dddinphp/last-wishes/tree/master/src/Lw/Domain/Model/User.
Alles in die folder is onderdeel van het model, niet alleen de class met de naam User.
Ik denk dat die repo het beter kan uitleggen dan ik dat zo kan bouwen, dat laat ook wat van die delegation-principes zien en voldoet helemaal aan het SOLID-principe.
Gewijzigd op 06/11/2020 07:20:25 door Thom nvt
Last Wishes bekeken: eigenschappen zijn daarin altijd private of protected en public wordt alleen gebruikt bij methoden.
Getters hebben vaak de naam van eigenschappen, (ingekort) bijvoorbeeld:
Kwestie van stijl vooral.
Gelijk on topic maar even de DDD-voorbeeldapplicatie Getters hebben vaak de naam van eigenschappen, (ingekort) bijvoorbeeld:
Code (php)
Kwestie van stijl vooral.
Een function userId() is wat arbitrair. In principe zou een functie altijd moeten omschrijven WAT het doet. In dit geval omschrijft de fuctienaam WAARMEE het iets doet (userId) maar niet wat de functie daar mee doet. Is het een setter, een getter, een generator? Dit is dus niet echt goed. Het kán wel, maar dan moet je dus als programmeur wel ervan op de hoogte zijn dat je op die manier een property aanroept (feitelijk dus gewoon een getter functie).
session_id() is een vergelijkbare combinatie van een accessor en mutator.
Ongebruikelijk is het niet: bijvoorbeeld de standaardfunctie Maar zou jij zo weten of een functie name() een setter of getter is? Ik niet. In de context van geschreven code uiteraard wel, omdat er dan een argument zal worden meegegeven of de functie wordt toegeschreven aan een variabele. Maar of dat nu echt duidelijk is? Je kunt het ook nog combineren.
Dan krijg je een setter en getter in één. Net als bij session_id(). Is op zich ook wel grappig :)
Gewijzigd op 06/11/2020 16:24:44 door Ozzie PHP
Daarnaast worden ValueObjects altijd immutable gemaakt, de waarde is de identiteit. Andere waarde betekent dus een nieuw object.
Opzich een goede opzet om immutability af te dwingen maar dan moet je van tevoren wel erg zeker zijn dat het nooit nodig is óf accepteren dat je het later moet aanpassen. Bij twijfel kies ik altijd protected, anders private.
Public gebruik ik nooit voor properties.
constructor property promotion wordt genoemd. Hiermee kun je, zoals de naam eigenlijk al zegt, parameters in de constructor promoveren tot eigenschappen.
PHP 7:
PHP 8:
PHP 8 heeft een nieuwe feature die PHP 7:
Code (php)
PHP 8:
Wat is daar het doel en/of voordeel van?
The properties are repeated 1) in the property declaration, 2) the constructor parameters, and 3) two times in the property assignment. Additionally, the property type is repeated twice.
Especially for value objects, which commonly do not contain anything more than property declarations and a constructor, this results in a lot of boilerplate, and makes changes more complicated and error prone.
This RFC proposes to introduce a short hand syntax, which allows combining the definition of properties and the constructor.
Code (php)
Daarnaast is er veel voor te zeggen om die validatie (overeenkomstig het single-responsibility principle) niet in te bouwen in het value object zelf maar te delegeren aan een aparte validator.
Maar stel dat je wel een id meegeeft aan de constructor, dan krijg je dus zo'n soort class?
Code (php)
Als ik nu die class bekijk, zie ik niet in één oogopslag dat er een propterty 'id' bestaat.
Je moet ook niet in een class kijken: je kijkt naar de public interfaces, waaronder de constructor om een object te creëren.
Ja, daar heb je een punt. Haha. Maar ben toch benieuwd hoe dit in de praktijk uitpakt. Van de ene kant wel mooi, maar van de andere kant zie je nu de property niet meer bovenin staan. Da's wel apart.
.