Database klasse ontwerpen
Naar aanleiding van een ander topic van mij, ontwerpen usermanagement open ik dit nieuwe topic. Aangezien ik in dit topic specifiek het ontwerp van de databaseclass wil bespreken. Iets wat toch een iets wat ander topic is.
Ik zou graag jullie mening krijgen over het ontwerpen van een database klasse. Ik wil hier toch wel wat aandacht aan besteden, aangezien dit toch één van de meest cruciale onderdelen van een applicatie is.
Om te beginnen wil ik Wouter citeren (citaat afkomstig uit het hierboven genoemde topic):
Wouter J op 06/07/2012 12:15:10:
Ook zou ik dit over meerdere objecten spreiden:
En misschien zelf een query object om je queries op te kunnen bouwen.
Code (php)
1
2
3
4
2
3
4
- Database (connectie en uitvoeren query)
- Result (het resultaat van een query)
- DataResult (resultaat van een select query)
- ...Result (resultaat van een UPDATE, DELETE of INSERT query)
- Result (het resultaat van een query)
- DataResult (resultaat van een select query)
- ...Result (resultaat van een UPDATE, DELETE of INSERT query)
En misschien zelf een query object om je queries op te kunnen bouwen.
Dit zelf vind ik wel een prettig model. Hoe ik dit praktisch zou implementeren, heb ik nog niet direct een idee van. Graag jullie meningen / ideeën / voorbeelden!
Even voor leesvoer hier nog twee topics waar redelijk wat tips in worden gegeven:
http://www.phphulp.nl/php/forum/topic/model-mvc-database-ophalen/85471/
http://www.phphulp.nl/php/forum/topic/oop-syntaxen-en-werkingen/85111/
Wat volgens mij belangrijk is in de eerste stap is duidelijk uitschrijven welke functionaliteit je wilt, welke verantwoordelijkheden er dus zijn en hoe je dat wilt onderverdelen in verschillende classes. Ik zie redelijk vaak voorbeelden voorbij komen waar een class meerdere verantwoordelijkheden krijgt en waardoor je dus heel veel aan flexibiliteit inlevert.
Ik ben van plan wat code te schrijven, hier te posten en dan kunnen we wellicht samen tot een beter resultaat komen.
Het topic heeft weliswaar MVC In de titel, maar het meeste wat er besproken wordt is niet specifiek MVC. Een goede database class (of set aan classes) kan je in bijna elke wijze van implementatie gebruiken.
Wat ik me in eerste instantie al afvraag, hoe behandelen jullie de configuratie instellen? Ikzelf dacht aan een .ini bestand te maken met daarin de basis gegevens voor de database en dit in de laden via een methode: readConfig. Deze geeft dan een array terug die ik op zijn beurt doorgeef aan een connect functie.
Dus de constructor zou er dan bijvoorbeeld zo kunnen uitzien:
Code (php)
1
2
3
4
5
2
3
4
5
<?php
public function __construct {
$this->databaseHandle = connect(readConfig());
}
?>
public function __construct {
$this->databaseHandle = connect(readConfig());
}
?>
Gewijzigd op 06/07/2012 16:30:45 door Write Down
Vervolgens ga je UML diagrammen tekenen, geef exact aan welke objecten welke properties en methods hebben en welke objecten een relatie hebben (HAS_A, IS_A, IMPLEMENT_A relaties).
Pas als je ALLES hebt uitgedacht begin je met scripten.
Tevens bestaan losse functies nooit in OO... Gebruik dus een Config klasse waarin je gegevens kunt setten en getten, vervolgens geef je die mee aan de Database klasse (HAS_A relatie) en die kan vervolgens de gegevens eruit halen.
Uitgetekend in UML wordt het dan:
Code (php)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
+-------------------------+ +-----------------------+
| Database | | Config |
+-------------------------+ +-----------------------+
| # config : Config | <----------------- | # settings : array |
+-------------------------+ +-----------------------+
| + init(config : Config) | | + set(setting, value) |
+-------------------------+ | + get(setting) |
+-----------------------+
| Database | | Config |
+-------------------------+ +-----------------------+
| # config : Config | <----------------- | # settings : array |
+-------------------------+ +-----------------------+
| + init(config : Config) | | + set(setting, value) |
+-------------------------+ | + get(setting) |
+-----------------------+
Gewijzigd op 06/07/2012 16:41:14 door Wouter J
Overigens, met jouw idee, dan krijg je dus iets als volgt:
Code (php)
Ik ga deze avond nog trachten een UML te maken. Hangt er vanaf hoeveel tijd ik heb. (moet straks nog weg)
Toevoeging op 06/07/2012 17:42:25:
Overigens, is het dan ook weer niet interessant om Config als een interface te bouwen en bv IniConfig die te laten implementeren e.t.c. ?
Gewijzigd op 06/07/2012 17:39:22 door Write Down
Wie weet wil je het volgend jaar in een XML bestand hebben, dan heb je met een interface het jezelf al makkelijk gemaakt.
Maar hoe je al die objecten aanmaakt zou ik niet zo doen. Als je het in elk geval echt op een OOP manier doet, dan heb je op je index pagina maar 1 regel staan en dat is het aanmaken van je basisobject (in MVC je controller). Vanuit dat object wordt de rest in gang gezet.
deze?
Misschien moet ik deze keer dan toch eerder naar MVC grijpen. Is al tijdje geleden, kan jij mij een bepaalde tutorial aanraden? Misschein Een MVC tutorial heb ik niet, ik heb het uit een boek gehaald (Head First Design Patterns - uberhaupt een enorme aanrader).
Maar goed, heb je dan een ergens een voorbeeld hoe ik het wel zou moeten ontwerpen? Want als je alles vanuit één object laat vertrekken, dan maak je de boel toch te veel van mekaar afhankelijk? Wat volgens mij nét niet de bedoeling kan zijn.
Nee, want in dat Applicatie object kun je alles mooi afhankelijk maken. Er is niks van elkaar afhankelijk, het enige wat afhankelijk aan elkaar is dat je website/project/applicatie afhankelijk is van 1 object: De Applicatie object/controller.
Dan zal ik mij daar eerst eens in moeten verdiepen :-)
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
<?php
$config = new Config();
$logger = new FileLog('log/database.log');
$database = new Database($logger, $config);
// Database::query() retourneert een DataRow object
// deze implementeert het IteratorAggregate interface
// zodat we hem straks zo in de foreach kunnen plaatsen
$result = $database->query('SELECT foo FROM bar');
foreach ($result as $key => $value) {
echo $key . ': ' . $value . "\n";
}
?>
$config = new Config();
$logger = new FileLog('log/database.log');
$database = new Database($logger, $config);
// Database::query() retourneert een DataRow object
// deze implementeert het IteratorAggregate interface
// zodat we hem straks zo in de foreach kunnen plaatsen
$result = $database->query('SELECT foo FROM bar');
foreach ($result as $key => $value) {
echo $key . ': ' . $value . "\n";
}
?>
Maak je hier een speciaal object voor aan:
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
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
<?php
class Application
{
protected $loggers = new StdClass();
public function __construct()
{
$this->loggers->database = new FileLog('log/database.log');
}
public function run()
{
$config = new Config();
$database = new Database($this->loggers->database, $config);
$result = $database->query('SELECT foo FROM bar');
foreach ($result as $key => $value) {
echo $key . ': ' . $value . "\n";
}
}
}
// index.php
$app = new Application();
$app->run();
?>
class Application
{
protected $loggers = new StdClass();
public function __construct()
{
$this->loggers->database = new FileLog('log/database.log');
}
public function run()
{
$config = new Config();
$database = new Database($this->loggers->database, $config);
$result = $database->query('SELECT foo FROM bar');
foreach ($result as $key => $value) {
echo $key . ': ' . $value . "\n";
}
}
}
// index.php
$app = new Application();
$app->run();
?>
Tevens vind ik ook hier dat een Service Container gebruiken erg handig zou zijn:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
// Application::construct();
$this->container = new Container();
$this->container->set('logger.class', 'FileLog');
$this->container->set('logger.destination', 'logs/database.log');
$this->container->set('logger', function($c) {
return new $c->get('logger.class')($c->get('logger.destination'));
});
$this->container->set('config.file', 'config/database.yml');
$this->container->set('config.parser', 'YmlParser');
$this->container->set('config.parse', function($c) {
$parser = new $c->get('config.parser')($c->get('config.file'));
if (!$parse instanceof ParserInterface) {
// fout afhandeling
}
return $parser->parse();
});
$this->container->set('config', function($c) {
$config = new Config();
foreach ($c->get('config.parse') as $key => $value) {
$config->set($key, $value);
}
return $config;
});
$this->container->set('database.class', 'PDOMySQLDatabase');
$this->container->set('database.config', $container->get('config'));
// dit is nog niet helemaal correct, dit hoort bij de Config service
$this->container->set('database.logger', $container->get('logger'));
$this->container->set('database', function($c) {
return new $c->get('database.class')($c->get('database.config'), $c->get('database.logger'));
});
// Application::run()
$database = $this->container->get('database');
$result = $database->query('SELECT foo FROM bar');
foreach ($result as $key => $value) {
echo $key . ': ' . $value . "\n";
};
?>
// Application::construct();
$this->container = new Container();
$this->container->set('logger.class', 'FileLog');
$this->container->set('logger.destination', 'logs/database.log');
$this->container->set('logger', function($c) {
return new $c->get('logger.class')($c->get('logger.destination'));
});
$this->container->set('config.file', 'config/database.yml');
$this->container->set('config.parser', 'YmlParser');
$this->container->set('config.parse', function($c) {
$parser = new $c->get('config.parser')($c->get('config.file'));
if (!$parse instanceof ParserInterface) {
// fout afhandeling
}
return $parser->parse();
});
$this->container->set('config', function($c) {
$config = new Config();
foreach ($c->get('config.parse') as $key => $value) {
$config->set($key, $value);
}
return $config;
});
$this->container->set('database.class', 'PDOMySQLDatabase');
$this->container->set('database.config', $container->get('config'));
// dit is nog niet helemaal correct, dit hoort bij de Config service
$this->container->set('database.logger', $container->get('logger'));
$this->container->set('database', function($c) {
return new $c->get('database.class')($c->get('database.config'), $c->get('database.logger'));
});
// Application::run()
$database = $this->container->get('database');
$result = $database->query('SELECT foo FROM bar');
foreach ($result as $key => $value) {
echo $key . ': ' . $value . "\n";
};
?>
Het is weer week, dus hebben we weer wat meer tijd :-)
Ik zie nu in de applicatie klasse dat je bepaalde zaken 'vast zet'. Daarmee bedoel ik de code die zorgt voor de query en de foreach. Ik denk dat dit in werkelijkheid toch niet de bedoeling kan zijn? Aangezien op index.php wil ik bijvoorbeeld één simpele query maar op mijn admin page wil ik er bijvoorbeeld 5.
Of is het zo dat je in die run() functie een of meerdere basis query's plaats die je altijd nodig hebt? En dat je daarna bv. $app->database->query('SELECT pint FROM beer'); uitvoert in beer.php?
Overigens wil ik je bedanken voor de code i.v.m. de container. Deze is voor mij zeer verhelderend en lijkt mij inderdaad praktisch om te gebruiken.
Ik zie nu in de applicatie klasse dat je bepaalde zaken 'vast zet'. Daarmee bedoel ik de code die zorgt voor de query en de foreach. Ik denk dat dit in werkelijkheid toch niet de bedoeling kan zijn? Aangezien op index.php wil ik bijvoorbeeld één simpele query maar op mijn admin page wil ik er bijvoorbeeld 5.
Of is het zo dat je in die run() functie een of meerdere basis query's plaats die je altijd nodig hebt? En dat je daarna bv. $app->database->query('SELECT pint FROM beer'); uitvoert in beer.php?
Overigens wil ik je bedanken voor de code i.v.m. de container. Deze is voor mij zeer verhelderend en lijkt mij inderdaad praktisch om te gebruiken.
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
interface ControllerInterface
{
// ...
}
class PageController implements ControllerInterface
{
// ...
}
class BlogController extends PageController
{
public function showAction($id)
{
// ... krijg de blogpost met id $id en stuur die door naar een template
}
public function showAllAction()
{
// ... krijg alle blogposts en stuur die naar een template
}
// ...
}
interface ControllerInterface
{
// ...
}
class PageController implements ControllerInterface
{
// ...
}
class BlogController extends PageController
{
public function showAction($id)
{
// ... krijg de blogpost met id $id en stuur die door naar een template
}
public function showAllAction()
{
// ... krijg alle blogposts en stuur die naar een template
}
// ...
}
- Wat is nu het verschil tussen Config en YamlConfig? Ze houden allebei settings vast in hetzelfde formaat. Ik denk dat je maar 1 Config object nodig hebt en dan een Parser interface moet maken, met meerdere parsers als YamlParser, XmlParser, enz. I.p.v. zelf een hele ingewikkelde YamlParser te maken raad ik aan gebruik te maken van het Yaml Symfony Component.
- Wat doet die addLogger in de controller? Dat lijkt me niet echt een taak van een controller. Het lijkt me handiger om de ControlerInterface abstract te maken met wat standaard functies als getDatabase() (om database object te krijgen), getConfig(), enz. Vervolgens maak je aparte Handlers, bijv. een ExceptionHandler.
- De container kun je natuurlijk simpel zelf maken (zoals Pim hier uitlegt), maar ik gebruik hiervoor altijd Pimple, misschien eens leuk om naar te kijken.
Verder wat betreft de parser, je hebt toch sowieso een niet erg ingewikkelde parser nodig? Ik ga er vanuit dat de configuratie iets vrij statisch is. Ik als ontwerpen van de applicatie zal kiezen welke instellingen er zijn en wat de mogelijke waarden zijn. Op zich kan ik die instellingen verwerken d.m.v. een array en dan yaml_emit. Wanneer ik de instellingen wil, kan ik dan yaml_parse_file gebruiken.
Uiteindelijk is dit toch eerder iets dat in de GUI wordt afgehandeld. (controleren wat voor een bepaalde instelling mogelijk is etc.)
Wat betreft de controllers, ook hier volg ik je mening. Alleen begrijp ik niet goed waarom je hier de handlers bij haalt. Wat zou je er dan willen mee willen doen bij de controllers?
Wat betreft de container, ik ben nu aan het bekijken of ik zelf iets leuk maak of inderdaad ga voor Pimple.
Quote:
En die parser zou ik dan bv. via de constructor kunnen meegeven. Of zie ik dat verkeerd?
Ik zou het gewoon weer in 2 dependencies opdelen (2 services) en die in de Container stoppen. 1 service is de parser, deze parsed het bestand en de andere is de Config die dan over de parse resultaten loopt en deze mooi met de set methods aan de Config toevoegt.
Quote:
je hebt toch sowieso een niet erg ingewikkelde parser nodig?
Je zult het bestand moeten parsen en voor Yaml heeft PHP nog geen eigen parse functies, voor zover ik weet. Ik raad daarom het gebruik van die YamlParser aan. Mocht PHP wel een goede Yaml parser hebben dan kun je die gewoon gebruiken en dan heb je geen Parse object meer nodig en heb je alleen een Parse service.
Quote:
Wat zou je er dan willen mee willen doen bij de controllers?
Met een Handler handel je een exception af bijv. Ik begreep niet echt goed waarvoor je de addLogger method had toegevoegd aan de Controller en dacht toen dat je dat deed om Exceptions/fouten af te handelen, vandaar dat ik begon over Handlers.
Quote:
Met een Handler handel je een exception af bijv. Ik begreep niet echt goed waarvoor je de addLogger method had toegevoegd aan de Controller en dacht toen dat je dat deed om Exceptions/fouten af te handelen, vandaar dat ik begon over Handlers.
Quote:
Wat zou je er dan willen mee willen doen bij de controllers?
Met een Handler handel je een exception af bijv. Ik begreep niet echt goed waarvoor je de addLogger method had toegevoegd aan de Controller en dacht toen dat je dat deed om Exceptions/fouten af te handelen, vandaar dat ik begon over Handlers.
Oorspronkelijk was dat niet zozeer mijn bedoeling. Maar nu heb jij mij wel op een idee gebracht. Maar alsnog, wat zou je dan doen met die handlers binnen de controller?