Validatie in controller én service classes?
Ik heb controllers zoals CustomerActivityController, en ik heb een service laag met services zoals CustomerActivityService. Stel dat ik een nieuwe customer activity wil maken via een API call in JSON format. De controller ontvangt een JSON string als request data, en de JSON wordt geparsed naar een array.
Code (php)
1
2
3
4
5
6
2
3
4
5
6
array(
'user_id' => 9,
'customer_id' => 123,
'activity' => 'Made a phone call',
'date' => '...'
)
'user_id' => 9,
'customer_id' => 123,
'activity' => 'Made a phone call',
'date' => '...'
)
Dus, mijn eerste stap (in controller) is het converteren van het input format (JSON of iets anders in de toekomst) naar universele data zoals een array.
Dan valideer ik de waardes in de array, gebruikmakend van Illuminate\Support\Facades\Validator.
In mijn controller method ziet dit er uit als:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?
$dataArray = \json_decode($request->input('json'), true);
$validator = Validator::make($dataArray, [
'user_id' => 'required|integer|gt:0',
'customer_id' => 'required|integer|gt:0',
'activity' => 'required|string',
'date' => 'required|date'
]);
if($validator->fails()) {
// ... error response.
}
// Input beschouwd als valide, ga verder...
$this->_customerActivityService->createActivity($dataArray);
?>
$dataArray = \json_decode($request->input('json'), true);
$validator = Validator::make($dataArray, [
'user_id' => 'required|integer|gt:0',
'customer_id' => 'required|integer|gt:0',
'activity' => 'required|string',
'date' => 'required|date'
]);
if($validator->fails()) {
// ... error response.
}
// Input beschouwd als valide, ga verder...
$this->_customerActivityService->createActivity($dataArray);
?>
Ik doe het overal op die manier.
Op de regel
Is $dataArray eigenlijk nog niet écht 'valid', omdat user_id of customer_id nog ongeldig kunnen zijn (id's bestaan misschien niet).
Ik zie dit als een aparte soort validatie, omdat dit niet gewoon rauwe data controleren is, maar meer richting de business logic gaat en samenhangt met andere data. Naar mijn mening zou dit dus zéker in een service layer moeten gebeuren, en in elk geval niet rechtstreeks in de controller.
Wat ik mezelf afvraag:
Ik heb nu validatie op verschillende plekken (in controller, en de businessdata-validatie in service). Ik zou de rauwe-data validatie gewoon kunnen verplaatsen naar mijn service class, maar dan is de service class 'tightly coupled' aan het request format (omdat de service dan een Request object of JSON string zou moeten krijgen als method parameters). Mijn service moet herbruikbaar zijn voor elk request type. (Dus dat ik verschillende controllers heb voor Web/API/..., maar dezelfde service class).
Ik vraag me dus af hoe ik de gegevens het beste kan doorgeven aan de service laag.
Een aantal opties die ik me kan bedenken:
1. Gewoon een array als input in de service accepteren:
Dit heeft echter het nadeel dat de array de correcte keys moet hebben en geen segment mag missen (de programmeur die mijn service class aanroept moet de samenstelling van de data array weten). Hiervoor moet je dus de klassedocumentatie raadplegen of de code van de service lezen.
2. Verschillende parameters voor elke 'eigenschap' van de data.
Code (php)
1
$this->_customerActivityService->createActivity($userId, $customerId, $activity, $date)
Voordeel: method signature is beschrijvend. Door de parameters is het meteen duidelijk wat de method nodig heeft. Nadeel: de parameter lijst kan extreem lang worden bij grotere data structuren, en je kunt op zich geen hierarchische data verwerken.
3. Accepteer een data object.
Dan maak ik me nog zorgen over het volgende:
Aangezien de rauwe-data validatie in de controller gebeurt, en de service doet de 'logic' validatie, komt het er op neer dat de service class zijn input 'vertrouwt'. De service class gaat er van uit dat de data die hij ontvangt al is gevalideerd, maar de service zou per ongeluk kunnen worden aangeroepen met input data die niet (juist) is gevalideerd. Zoals -12 doorgeven als $user_id in een service method call. Vandaar dus dat ik de neiging krijg om gewoon alle validatie (dus ook raw-data validatie) in de service class te plaatsen.
Natuurlijk moet de collega die de service class aanroept weten wat hij doet, maar bij OOP design is het ook juist de bedoeling dat je het open-closed principe toepast en samenhangende logica zo dicht mogelijk bij elkaar plaatst.
Heeft er iemand ervaring met deze hardcore OOP, en MVC architectuur?
Gewijzigd op 20/05/2022 21:09:51 door Mark Hogeveen
Van MVC ben ik ook geen fan omdat het enorm misbruikt wordt. Het is in de jaren 80 ontwikkeld voor complete applicaties, en met PHP doe je toch echt iets anders. Het illustreert wel de kern van je probleem.
In jouw geval (en in het onze) is PHP niet de eigenaar van de data. Dat is de database. Dus je weet eigenlijk nooit of een database ID in PHP geldig is, omdat na de transactie van het opvragen een record verwijderd of gemuteerd kan zijn. Daarmee gaat ook al meteen het hele originele idee van MVC verloren. Komt nog bij dat je in PHP doorgaans niets met een interface doet, dat doe je dan in de browser, waar JavaScript draait in een aparte thread op een andere machine, en je ziet al snel dat je praktisch niets aan het hele MVC-gedachtengoed hebt. Wel heeft het geleid tot een wereldwijde babelonische spraakverwarring over wat MVC zou moeten zijn, iedereen ziet er weer wat anders in.
Ik ben ook geen fan meer van OOP, design patterns kunnen beter en efficiënter. Een zo'n pattern dat voor verbetering vatbaar is, is die van dependency injection, die in combinatie met het woord 'hell' interessante links oplevert in de zoekmachine. Ik geloof dat Laravel daar service containers voor heeft bedacht om van DI nog chocolade te kunnen maken, maar laten we eerlijk wezen, het levert niet bepaald efficiëntere programma's op.
Een ander ding waar ik zo m'n vraagtekens bij heb is PHP zelf. Het lijmt alles aan alles, en de valkuil is dat het simpel lijkt, maar ondertussen moet je wel alle mitsen en maren weten van wat je aan wat lijmt, anders krijg je vroeg of laat alleen maar meer problemen. 'Universele arrays' is zo'n plakmiddel. Het lijkt op JSON, en het heeft hetzelfde probleem als NoSQL-databases; er zit totaal geen controle op. Niet dat je die al had, want de data is van de database, dus zou ik het volgende doen.
PHP is voor de webapp een interface tot de database. Als je een array krijgt aangeboden, kan je in PHP alleen op basis van de metadata uit de database een validatie uitvoeren. Zoals dat een ID aanwezig moet zijn, niet NULL mag zijn, een 64-bits integer moet zijn, en doorgaans groter dan 0. Die metadata hoef je niet te programmeren zoals met ORM (dat is de omgekeerde methode, die raad ik af als om de data geeft), je kan het direct uit de database opvragen.
Uiteindelijke validatie vindt plaats nadat je de database hebt bevraagd of het record bestaat, en wat de bijbehorende gegevens uit het record zijn op basis van het ID. En dan nog is het niet real-time, een halve seconde later kan het record door een collega gewist zijn, tenzij je hiervoor zelf logica inbouwt in de database.
Gezien dat de database de eigenaar is van de data, is het ook de database die verantwoordelijk is voor de integriteit en dus validatie van gegevens. Hiervoor bouw je constraints in de database. In mijn database zijn de constraints voorzien van comments, die ik als foutmelding gebruik wanneer een constraint voorkomt dat er ongeldige data wordt ingevoerd.
Niet alleen is die methode efficiënt in gebruik, het scheelt ook veel code.