[OOP] DRY bij UserMappers. Exceptions, een lastig concept?
Inmiddels al weer aardig wat topics gelezen en opgezocht van de beginner tot de expert. Het is alleen altijd lastig om de voor jou beste methode uit te kiezen. Ik zit momenteel namelijk met 2 problemen.
Een daarvan is de alom bekende foutafhandeling of liever gezegd het opvangen van uitzonderingen. Ik heb inmiddels begrepen dat ik in de meeste gevallen Exceptions kan gebruiken en dat ik er verstandig aan doe om meerdere catch blokken te gebruiken om zo gebruikersfouten en systeemfouten te kunnen onderscheiden.
Momenteel heb ik het zo gedaan dat mijn autoloader binnen een try blok staat. Dat houd dus in dat mocht er in een van de classes wat fout gaan, dan wordt het in ieder geval opgevangen.
Mijn punt, zodra er dus iets fout gaat stop hij al het andere wat er binnen het try blok gebeurt, zo dus ook de knoppen die ik bijvoorbeeld weer laat geven onder een error.
In een situatie als een CMS dat er geen gebruikers gevonden kunnen worden, maar ik wil er nog wel een kunnen toevoegen. Dan moet niet ineens die knop weg zijn. Zou dit inhouden dat ik dan gewoon mijn try en catch blokken anders moet doen? Of kan ik aan de hand van levels wellicht zeggen dat hij de rest gewoon uit moet voeren? Of geen exception gebruiken, maar iets totaal anders, iets van mijzelf? En de exceptions laten voor de pure fouten die alles moeten stoppen.
Mijn andere probleem ligt bij het zo min mogelijk herhalen van code. Het liefst schrijf ik het maar 1 keer. Althans dat is mijn doel.
Maar neem bijvoorbeeld mijn UserMapper, die is praktisch hetzelfde als mijn PageMapper behalve dat er een andere factory method inzit is hij gelijk. Is er geen methode om dat over te erven zonder dat mijn flexibiliteit naar de maan gaat? Denk bijvoorbeeld aan een abstracte classe?
De autoloader laad alleen bestanden, hij voert ze niet uit. Dit gaat het dus niet verhelpen. Beter is het om een exception handler in te stellen (set_exception_handler), deze wordt aangeroepen wanneer een exception niet wordt opgevangen.
>> Zou dit inhouden dat ik dan gewoon mijn try en catch blokken anders moet doen? Of kan ik aan de hand van levels wellicht zeggen dat hij de rest gewoon uit moet voeren? Of geen exception gebruiken, maar iets totaal anders, iets van mijzelf? En de exceptions laten voor de pure fouten die alles moeten stoppen.
Ik zou gaan voor die eerste. Iets zelf bouwen moet je alleen in echt hele specifieke gevallen doen, als je echt hele goede redenen hebt. Redenen die zo goed zijn dat als ik je 's nachts om 3 uur wakker maak je ze mij moet kunnen vertellen. :)
>> Is er geen methode om dat over te erven zonder dat mijn flexibiliteit naar de maan gaat? Denk bijvoorbeeld aan een abstracte classe?
Ja, top idee! :D
De Exceptions. Bij alle methodes die dus eigenlijk een Gelukt of Niet gelukt waarde moeten terugsturen ga ik nu dus niet meer de zin terugsturen. Ik stuur gewoon TRUE of FALSE. Zo maak ik die zinnen waar ik vroeger een notice blok voor gebruikte nu pas op de view pagina. Grote voordeel is dus dat ik geen exceptions hoef te gooien.
De exeptions die ik dan nog gooi zijn hoogstwaarschijnlijk fataal, denk dan bijvoorbeeld aan Query mislukt, Map niet gevonden dus code kapt ermee.
Bij formulieren kan ik dan zeggen dat ik de try catch blokken al in de class maak zodat hij al eerder opgevangen wordt en niet pas als alles uitgevoerd moet zijn.
Je kunt je try blok ook "kleiner" maken en de knoppen onder je try/catch blok zetten:
Code (php)
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
<?php
try {
$user = new User();
} catch (Exception $e) {
// geen user aangemaakt
}
// toon hier je knoppen
?>
try {
$user = new User();
} catch (Exception $e) {
// geen user aangemaakt
}
// toon hier je knoppen
?>
Ongeacht of er wel of niet een user wordt aangemaakt, zullen de knoppen altijd worden getoond.
Maar ik heb het nu redelijk in mijn hoofd zitten wat het moet gaan worden, dus denk dat het wel goed komt.
Ik moet zeggen ik loop precies een aantal stappen achter jou aan geloof ik, want ik ontdek steeds redelijk recente topics van jou als ik een vraagstuk heb. Zo nu en dan verduidelijking of bevestiging vragen en ik kan weer verder. Heel prettig.
Als je een class foo hebt, dan gooi je vanuit die class een FooException. Dan weet je ook WAAR de exception vandaan komt. Je kunt ook nog een foutcode meegeven een een exception.
Gewijzigd op 27/02/2014 22:06:41 door Ozzie PHP
Of zie ik dat verkeerd?
Per class vind ik wat overdreven, ik doe het vaak per onderwerp. Dat komt ook doordat ik de Doctrine stijl van exceptions gebruik: Exceptions aanmaken in een factory method.
Om maar in het verhaal van een database te blijven, wanneer er geen resultaten gevonden zijn is dat een uitzondering voor de find method, dus dan maken we een exception: NoResultsFoundException:
Code (php)
Je database heeft ook verschillende exceptions wanneer het geen connectie kan maken. Dit gaat over 1 onderdeel en is dus 1 exception (dit gaat een beetje op gevoel). Wel zorg ik altijd dat elke message zijn eigen exception code krijgt:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
namespace Milo\Database\Exception;
class ConnectionException extends \RuntimeException implements MiloDatabaseException
{
const SERVER = 1;
const AUTH = 2;
public static function noServerConnection($server, $message = null)
{
return new self(sprintf(
'Failed to connect to server "%s"%s', $server, $message ? ': '.$message : ''
), self::SERVER);
}
public static function wrongCredits($server, $credits)
{
return new self(sprintf(
'Wrong credentials ($credits) to connect to server "%s".', $credits, $server
), self::AUTH);
}
}
?>
namespace Milo\Database\Exception;
class ConnectionException extends \RuntimeException implements MiloDatabaseException
{
const SERVER = 1;
const AUTH = 2;
public static function noServerConnection($server, $message = null)
{
return new self(sprintf(
'Failed to connect to server "%s"%s', $server, $message ? ': '.$message : ''
), self::SERVER);
}
public static function wrongCredits($server, $credits)
{
return new self(sprintf(
'Wrong credentials ($credits) to connect to server "%s".', $credits, $server
), self::AUTH);
}
}
?>
>> Of zie ik dat verkeerd?
Het is in ieder geval al goed dat je er over nadenkt. Nee, het heeft geen zin om de bestandsnaam mee te sturen. Je wil in je code kunnen zeggen... als vanuit class X een exception wordt gegooid dan wil ik die opvangen, maar als vanuit class Y een exception wordt gegooid dan laat ik 'm door gaan.
Stel jij wil iets doen met class Foo. Je zet het in een try block. Maar class Foo maakt gebruik van class Bar en class Bar maakt weer gebruik van class FooBar. Nu gooit class FooBar ineens een Exception. Als je met exceptions per class werkt, dan kun je gericht die exception opvangen, of misschien juist wel niet.
Ik kijk dus vooral WAAR de fout zich voordoet en Wouter kijkt vooral WAT er fout gaat. Voor beide methodes valt iets te zeggen. Het nadeel aan de WAT methode vind ik dat je niet weet waar ie vandaan komt. Persoonlijk vind ik dat niet fijn, want als ik niet weet waar de fout vandaan komt, hoe moet ik er dan actie op ondernemen.
Gewijzigd op 28/02/2014 00:29:00 door Ozzie PHP
Persoonlijk voel ik denk ik wel meer voor wat Wouter doet, ik bedoel als ik weet waar iets vandaan komt vind ik dat minder zaligmakend dan dat ik weet wat er fout gaat. Een bestandsnaam voor waar het fout gaat, Exceptions eigen getfile methode, dan zal dat voor mij genoeg zijn.
Op de nodige plekken maak ik gewoon extra Try - Catch combinaties zodat niet alles omver gaat. Op mijn hoofd Try - Catch combinatie, welke om de include van de uitvoer pagina's heen staat, wil ik een extra catch blok om de verschillende fouten op te kunnen vangen. Zoals Gebruikersgeralteerd en Systeemgerelateerd. Al denk ik dat ik dat zelfs bij de meeste zou gebruiken. Anders is het misschien al te laat.
Verder bekijk ik de resultaten van Queries en dingen gewoon met TRUE of FALSE. Dus als er TRUE komt kan ik eventueel bericht tonen dat het goed is gegaan met verwijderen of de data laten zien met ophalen. Daar ga ik dus geen Exceptions voor gebruiken, ook al was dat wel het idee eerst.
Gewijzigd op 28/02/2014 09:02:31 door Milo S
Oké. Met zoveel dingen is het een persoonlijke keuze hoe je het aanpakt. Als je er vooral maar erg goed over nadenkt. Stel jij gooit ergens een WAT exception, bijv. een FileNotFoundException of iets dergelijks. Weliswaar weet je nu WAT er fout gaat, maar je weet niet WAAR de fout optreedt. Stel dat een belangrijk configuraitebestand niet kan worden ingeladen, dan heb je een dik probleem. Echter, is het simpelweg een cachebestand wat nog niet is aangemaakt, dan is er niks aan de hand. Als je dus niet weet WAAR de fout vandaan komt, kun je ook geen passende maatregelen treffen. En tuurlijk, dan kun je wel de file ophalen, maar daar heb je niks meer aan, want je hele applicatie is al gestopt. Bovendien, je wil toch niet op 1 centraal punt de filenamen binnenhalen en dan per filenaam gaan beslissen wat er moet gebeuren? Krijg je straks een switch met 100 opties :)
En als je in dit geval een zeer belangrijk configuratie bestand wilt laden en je krijgt een FileNotFoundException, dan ga je die natuurlijk niet catchen. Als je echter de cache niet kan laden dan catch je hem wel, om vervolgens de cache opnieuw aan te maken. Daarvoor hoef je niet te weten waar die exception vandaan komt, je moet alleen de execution context weten (en die weet je).
Gewijzigd op 28/02/2014 16:13:49 door Wouter J
Snap ik. Dat is om achteraf actie te ondernemen.
>> En als je in dit geval een zeer belangrijk configuratie bestand wilt laden en je krijgt een FileNotFoundException, dan ga je die natuurlijk niet catchen.
Dat is dus wat ik bedoel. Hoe weet je om welke exception het gaat? Als je een wat groter try/catch blok hebt dan kunnen ze misschien allebei voorkomen, maar je vangt af op type. Ze worden dus beide opgevangen. Maar nogmaals, het is dus een kwestie van persoonlijke voorkeur en werkwijze.
Nee, want de 1 vang je als het goed al veel eerder op dan de globale try..catch.
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class ConfigLoader
{
public function load(...)
{
try {
return $this->cacher->getConfig(...);
} catch (FileNotFoundException $e) {
// waarschijnlijk nog geen cache, laten we die aanmaken
$config = $this->doLoad(...);
// ai, als deze (-^) een FileNotFoundException gooit vangen
// we die niet op, er is iets belangrijks: config bestand
// is niet aanwezig!
$this->cacher->cacheConfig($config);
}
}
}
?>
class ConfigLoader
{
public function load(...)
{
try {
return $this->cacher->getConfig(...);
} catch (FileNotFoundException $e) {
// waarschijnlijk nog geen cache, laten we die aanmaken
$config = $this->doLoad(...);
// ai, als deze (-^) een FileNotFoundException gooit vangen
// we die niet op, er is iets belangrijks: config bestand
// is niet aanwezig!
$this->cacher->cacheConfig($config);
}
}
}
?>
Gewijzigd op 28/02/2014 16:31:09 door Wouter J
Dat kan ook. Kwestie van werkwijze dus. Ik zal zelf ook nog eens over nadenken wat het handigst is.
Het voorbeeld wat ik hier geef is wellicht niet helemaal reëel, maar het gaat me even om de achterliggende gedachte hoe je met een dergelijke situatie moet omgaan als je geen gebruik maakt van WAAR exceptions maar wel van WAT exceptions.
Stel we willen een of ander cache bestand laden:
Code (php)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<?php
try {
$foo = new Foo();
$cache = $foo->getFooCache();
} catch (FileNotFoundException $e) {
// file niet gevonden, data inladen
}
?>
try {
$foo = new Foo();
$cache = $foo->getFooCache();
} catch (FileNotFoundException $e) {
// file niet gevonden, data inladen
}
?>
We hebben hier dus een WAT exception. WAT kan er fout gaan? Een file wordt niet gevonden.
Hoe waarschijnlijk het onderstaande scenario is, laten we het daar even niet over hebben. Het gaat mij erom hoe je hier mee zou moeten omgaan.
Wat je wil bereiken is dus dat als de cache data niet wordt gevonden, je deze alsnog gaat inladen.
Stel nu dat de Foo class gebruik maakt van een configuratiebestand om te bepalen waar hij het cache-bestand moet zoeken. Oeps, om een of andere reden bestaat het configuratiebestand niet meer en de filesystem class gooit een FileNotFoundException. Deze exception is zo ernstig, dat we deze helemaal naar boven willen laten opborrelen. Maar o jee, hij wordt opgevangen door het catch-blok hierboven, terwijl dat niet de bedoeling is.
Hoe los je zo'n situatie op?
Gewijzigd op 01/03/2014 17:21:10 door Ozzie PHP
Dat plaatsen we dan niet in een try en catch blok. Tevens wordt het afhandelen van de cache als het goed is al in Foo#getFooCache gedaan.
Maar dat lijkt me dus een lastige. Je roept een functie aan en die functie moet ergens een path vandaan halen om te kijken waar het bestand moet worden opgehaald. Het config bestand wordt niet gevonden, en die exception wordt dus wel opgevangen. Nou kun je zeggen dat het niet handig geprogrammeerd is (ben ik met je eens) maar de situatie kan zich wel voordoen.
Of een ander voorbeeld. Het bestand van een class kan niet worden ingeladen. De autoloader gooit een FileNotFoundException. Deze zou dan ook weer door dit try/catch blok worden opgevangen wat niet de bedoeling is.
Voor de goede orde... het gaat er mij niet om wie gelijk heeft. Ik probeer uit te vinden of het voor mijzelf een optie is om ook met WAT exceptions te werken ipv WAAR exceptions, omdat ik in het 1e geval minder exception classes hoef te maken. Alleen ik wil graag weten of ik dan op een goede manier alle situaties kan afhandelen.
Bij deze ga ik nu dus beginnen met het bouwen van Try en Catch blokken binnen mijn DataMappers e.d. Zo heb ik het eerste al gewonnen namelijk de PDOException en voor in de autoloader een FileNotFound Exception. Langzaamaan verder borduren op de mogelijk exceptions die ik krijg maakt straks een redelijk compleet systeem lijkt mij.
Edit
Nu ik hier dus aan wil beginnen lijkt het mij anderzijds ook heel onlogisch. Bijvoorbeeld in mijn DatabaseStorage, daar waar de Query wordt uitgevoerd. De functie execute retourneert TRUE of FALSE. Dan hoef ik toch geen exception meer te gooien? Bedoel de DataMapper krijgt een FALSE terug dus die retourneert dat gewoon weer naar mijn view pagina. Welke op zijn buurt weet dat er iets is fout gegaan.
Het enigste wat hij niet weet is wat er fout is gegaan, dat zou ik kunnen zien met die exception.
Is het dan geen idee om een soort van DEBUG mode aan en uit te kunnen zetten, of te loggen natuurlijk. Dus dat de exceptionhandler ook nog een FALSE retourneert naar de DatabaseStorage welke dat retourneert naar de DataMapper en uiteindelijk de view. En dan ondertussen de data gewoon opslaat. Zo kan ik per view pagina zelf een melden weergeven wat ik wil bijvoorbeeld Gebruikers niet gevonden of iets dergelijks, maar wordt er wel een goede fout opgeslagen.
Gewijzigd op 01/03/2014 18:20:28 door Milo S
Als een autoloader een bestand niet kan vinden gooit ie een ClassNotFoundException, eventueel met de FileNotFoundException als previous exception.
>> Het config bestand wordt niet gevonden, en die exception wordt dus wel opgevangen.
Die wordt niet opgevangen:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Router
{
protected function getMatcher()
{
try {
return $this->cache->load('ProjectRouteMatcher.php');
} catch (FileNotFoundException $e) {
// geen cache
$routes = $this->loadRoutes(); // als deze een FileNotFoundException gooit wordt die niet opgevangen
$this->cache->write('ProjectRouteMatcher.php', $this->matcherDumper->dump($routes);
}
}
}
?>
class Router
{
protected function getMatcher()
{
try {
return $this->cache->load('ProjectRouteMatcher.php');
} catch (FileNotFoundException $e) {
// geen cache
$routes = $this->loadRoutes(); // als deze een FileNotFoundException gooit wordt die niet opgevangen
$this->cache->write('ProjectRouteMatcher.php', $this->matcherDumper->dump($routes);
}
}
}
?>
Duidelijk!
>> Die wordt niet opgevangen:
Dit is een ander voorbeeldje. Wat ik bedoel is dat je een method aanroept die een FooBar exception kan gooien. Deze method maakt gebruik van een andere class. Die andere class kan ook een FooBar exception gooien. De eerste FooBar exception wil je wel opvangen, maar die van die andere class niet, want die moet opborrelen naar boven. Hoe voorkom je nu dat die niet wordt opgevangen?