OOP fouten + records
Quote:
If the last SQL statement executed by the associated PDOStatement was a SELECT statement, some databases may return the number of rows returned by that statement. However, this behaviour is not guaranteed for all databases and should not be relied on for portable applications.
(bron)
(bron)
Daarom. Wouter, heb je verder nog antwoord op (een van) mijn vragen?
Wouter J op 04/07/2012 11:20:15:
Roel PHP op 14/07/2012 23:45:30:
Ten tweede, ik heb nu een abstracte mapper class gemaakt, want elke mapper moet toch bepaalde methods krijgen, anders is het geen mapper. Ik wou er eigenlijk een interface van maken
Dit zou ik beide doen. Een abstracte basis class en een interface zijn twee verschillende dingen die (deels) een ander doel dienen.
De abstracte basis class is handig om een deel van de functionaliteit te centraliseren zodat je niet hetzelfde stuk code in verschillende classes hebt. Scheelt code, scheelt werk en maakt het eenvoudiger om fouten weg te werken.
Een interface heeft als primaire doel om bepaalde functionaliteit voor te defineren. Door via een interface bepaalde methodes af te dwingen kan je op een veilige manier afhankelijkheden leggen. Omdat een class een bepaalde interface implementeert weet je dat je de methodes uit die interface in de class kan aanroepen (vanuit een andere class). Stel je wil vandaag je log in systeem bouwen op basis van een database, maar morgen wil je over stappen op tekst bestanden. Als beide datamappers dezelfde interface implementeren (met of zonder dezelfde abstracte basis class te hebben) kan je dat eenvoudig doen.
In het kort is het dus handig om alle datamappers dezelfde abstracte basis class te geven om bepaalde code te centraliseren en is het handig om interfaces te schrijven voor elke functionele datamapper (je hebt dus verschillende interfaces voor een user datamapper en een product datamapper).
Wat betreft de implementatie van een abstracte methode. Bij mijn weten moet je een abstracte methode precies zo implementeren als aangeven in de abstracte definitie. Je kan dus niet een variabele toevoegen.
Quote:
Wat betreft de implementatie van een abstracte methode. Bij mijn weten moet je een abstracte methode precies zo implementeren als aangeven in de abstracte definitie. Je kan dus niet een variabele toevoegen.
Laatst had ik iets interessants gelezen. Je moet namelijk een abstracte definitie ervoor zorgen dat je het op dezelfde manier kan aanroepen. Dus bijv:
Code (php)
Dit zal niet werken. PHP zal Baz::bar aanroepen met 2 argumenten en een error krijgen omdat je nog een derde argument hebt toegevoegd. Maar maak je die 3e optioneel dan kun je Baz::bar gewoon aanroepen met 2 argumenten, maar ook met 3:
En je kan een functie altijd zoveel argumenten meegeven als je wilt en dan kun je ook nog gaan werken met func_get_args, mocht je dat handiger/leuker vinden.
Code (php)
1
2
3
4
5
2
3
4
5
<?php
$pdo = new PDO('mysql:host=localhost;dbname=roelvdwater', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db = new Database($pdo);
?>
$pdo = new PDO('mysql:host=localhost;dbname=roelvdwater', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db = new Database($pdo);
?>
Ik vind het overigens eerlijk gezegd overdreven om zoveel classes en interfaces te maken voor één object. Je hebt straks per objects drie classes nodig: het object zelf, een datamapper en datadatamapper...?
Kan ik wel zoiets doen? Dit lijkt mij de beste oplossing:
MapperInterface
Code (php)
Mapper
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
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
<?php
require 'mapperinterface.php';
/**
* Basic functionality for mapper objects
*
* @author Roel van de Water
*/
abstract class Mapper implements MapperInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function create()
{
// Implementation at child class
}
public function save()
{
// Implementation at child class
}
public function delete()
{
// Implementation at child class
}
public function populate(array $result)
{
// Implementation at child class
}
}
?>
require 'mapperinterface.php';
/**
* Basic functionality for mapper objects
*
* @author Roel van de Water
*/
abstract class Mapper implements MapperInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function create()
{
// Implementation at child class
}
public function save()
{
// Implementation at child class
}
public function delete()
{
// Implementation at child class
}
public function populate(array $result)
{
// Implementation at child class
}
}
?>
UserMapper
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
require 'mapper.php';
/**
* Class for communicating between the user and database object
*
* @author Roel van de Water
*/
class UserMapper extends Mapper
{
// getByUsernameAndPassword...
public function create(User $user)
{
}
public function save(User $user)
{
}
public function delete(User $user)
{
}
public function populate(array $result)
{
}
}
?>
require 'mapper.php';
/**
* Class for communicating between the user and database object
*
* @author Roel van de Water
*/
class UserMapper extends Mapper
{
// getByUsernameAndPassword...
public function create(User $user)
{
}
public function save(User $user)
{
}
public function delete(User $user)
{
}
public function populate(array $result)
{
}
}
?>
Dit ziet er in mijn ogen vrij correct uit, alleen krijg ik zo deze error:
SCREAM: Error suppression ignored for
Fatal error: Declaration of UserMapper::create() must be compatible with MapperInterface::create() in \classes\usermapper.php on line 9
Quote:
Ik vind het overigens eerlijk gezegd overdreven om zoveel classes en interfaces te maken voor één object. Je hebt straks per objects drie classes nodig: het object zelf, een datamapper en datadatamapper...?
Kan ik wel zoiets doen?
Kan ik wel zoiets doen?
Ja, hoe meer klassen hoe beter eigenlijk. Laatst had ik eens getest hoeveel klassen je eigenlijk gebruikt voor een simpele pagina aanroep in Symfony: 256. Je moet dus echt niet bang zijn veel klassen te maken. Kijk bijv. ook eens hoe ik dat scriptje heb gemaakt om klassen te tellen: http://snipplr.com/view/66144/classfilteriterator/ Daarin zie je ook dat ik 6 klassen/interfaces gebruik om dat voor elkaar te krijgen.
En goed, kijk eens wat je doet. In de MapperInterface zeg je:
In de UserMapper zeg je:
Zie jij het verschil? Ik wel.
En nu ziet PHP die Interface die geen argumenten heeft. Om te testen of UserMapper wel aan het interface voldoet gebruikt PHP:
En woeps, hij krijgt een error.
Je moet er dus voor zorgen dat je UserMapper::create() kan aanroepen zonder argumenten of je moet in de interface aangeven dat de functie create() 1 argument nodig heeft.
Wat ik zelf doe in mijn Storage klassen die ik nu aan het maken ben is dat ik alle elementen die ik kan opslaan een interface meegeef: Storable. Vervolgens geef ik in het hoofdinterface van de datamapper (in mijn geval StorageInterface) aan dat de functie create een Storable moet 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
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
<?php
// DataMappers
interface MapperInterface
{
// ...
public function create(Storable $store);
}
abstract class DatabaseMapper implements MapperInterface
{
// ... methods die database opzetten, ect.
}
class UserMapper extends DatabaseMapper
{
public function create(Storable $user)
{
// ... sla nieuwe gebruiker op
}
}
// Users
interface Storable
{
// overeenkomstige methods
}
class User implements Storable
{
// ... user methods
}
// Bootstrap
$pdo = new PDO(...);
$database = Database::getDBO('pdo:mysql');
$userMapper = new UserMapper($database);
$user = new User(...);
$userMapper->create($user);
?>
// DataMappers
interface MapperInterface
{
// ...
public function create(Storable $store);
}
abstract class DatabaseMapper implements MapperInterface
{
// ... methods die database opzetten, ect.
}
class UserMapper extends DatabaseMapper
{
public function create(Storable $user)
{
// ... sla nieuwe gebruiker op
}
}
// Users
interface Storable
{
// overeenkomstige methods
}
class User implements Storable
{
// ... user methods
}
// Bootstrap
$pdo = new PDO(...);
$database = Database::getDBO('pdo:mysql');
$userMapper = new UserMapper($database);
$user = new User(...);
$userMapper->create($user);
?>
Edit:
En de DatabaseMapper verwijst natuurlijk weer naar een Database object die weer verschillende kinderen heeft als MySQLiDatabase, abstract PDODatabase, PDOMySQLDatabase, PDOOracleDatabase, ect. Die de database klasse aanmaakt door een factory method.
Hierdoor kom je uit op zo'n 9 klassen/interfaces voor het opslaan van een gebruiker :)
Hierdoor kom je uit op zo'n 9 klassen/interfaces voor het opslaan van een gebruiker :)
Gewijzigd op 16/07/2012 14:27:06 door Wouter J
Dus ipv User zeg je Storable, ipv Page zeg je Storable?
Precies wat ik zoek denk ik! Ik ga ermee aan de slag en als ik vragen heb laat ik het weten!
Bedankt voor de tips.
Overigens snap ik je Database::getDBO()-method niet echt. Wat is hier het nut van?
Quote:
Dus in feite geef je nu gewoon altijd de parent class mee aan je methods?
Dus ipv User zeg je Storable, ipv Page zeg je Storable?
Dus ipv User zeg je Storable, ipv Page zeg je Storable?
En dat is nou precies waarom Erwin en ik zo zitten te hameren op het maken van parent klassen/superklassen. Het maakt je niks uit welke klasse je nou gaat opslaan, als het maar een instance is van Storable. In Storable leg je vast welke methods die klassen per se nodig heeft en die methods kun je dus gewoon altijd gebruiken. Stel je voor dat je een method getData maakt om de data te krijgen dan weet je in de UserMapper::create() method nu zeker dat je die method kan gebruiken.
Quote:
Overigens snap ik je Database::getDBO()-method niet echt. Wat is hier het nut van?
Dat is een factory zoals we dat noemen. In die method maak je de juiste database klasse aan die je kan gebruiken. Bijv:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Dus je maakt dan niet meer zelf een Database object aan, maar je maar een static method die er geen een aanlevert. Interessant. En die PDOMySQLDatabase etc. maak je gewoon zelf?
Quote:
En die PDOMySQLDatabase etc. maak je gewoon zelf?
Ja.
En dat zijn als ik het goed begrijp zeker ook weer een childs van een andere interface om te kunnen bepalen wat voor methods erin moeten komen, alleen moet de implementatie per database anders zijn?
Ja, je hebt 1 database klasse met queries. Die database klasse heeft weer subklassen die per adapter ervoor zorgen dat het uitgevoerd word. Want een query uitvoeren is natuurlijk verschillend per adapter.
Je doet normaal toch via een method in de mapper iets als $this->db->query().
Maar het argument van die method is dan altijd hetzelfde, welke database hem ook afhandelt. Hoe doe je dat dan verder?
Ik heb trouwens m'n try-catch probleem nog niet opgelost. Weet jij hoe dit te fixen is?
Overigens heb ik nu een Storable interface zoals je zei. Moet ik nu m'n methods in m'n UserMapper die een User object veranderen in Storable, of gewoon zo laten? Het kan beide, maar wat is het beste?
Edit:
Laat maar, het moet perse Storable zijn.
Gewijzigd op 17/07/2012 17:47:49 door Roel -
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
/**
* A interface for all Database classes
*/
interface Database
{
public function connect($host, $user, $pass, $db);
public function query($query, array $parameters = array());
public function fetch($result);
}
/**
* The MySQL class
*/
class MySQLDatabase implements Database
{
protected $link;
public function connect($host, $user, $pass, $db)
{
$this->link = mysql_connect($host, $user, $pass);
// error handling
mysql_select_db($db, $this->link);
// error handling
}
public function query($query, array $parameters = array())
{
$query = vsprintf(str_replace('?', '%s', $query), $parameters);
return $this->fetch(mysql_query($query, $this->link));
}
public function fetch($result)
{
return mysql_fetch_assoc($result);
}
}
/**
* The PDO superclass
*/
abstract class PDODatabase implements Database
{
protected $link;
public function query($query, array $parameters = array())
{
$stmt = $this->link->prepare($query);
return $this->fetch($stmt->execute($parameters));
}
public function fetch($result)
{
return $result->fetch(PDO::FETCH_ASSOC);
}
}
/**
* The MySQL PDO class
*/
class PDOMySQLDatabase extends PDODatabase
{
public function connect($host, $user, $pass, $db)
{
$this->link = new PDO('mysql:host='.$host.';dbname='.$db, $user, $pass);
// error handling
}
}
/**
* The Oracle PDO class
*/
class PDOOracleDatabase extends PDODatabase
{
public function connect($host, $user, $pass, $db)
{
// maak connectie op de Oracle manier
$this->link = new PDO(...);
// error handling
}
}
/**
* A interface for all Database classes
*/
interface Database
{
public function connect($host, $user, $pass, $db);
public function query($query, array $parameters = array());
public function fetch($result);
}
/**
* The MySQL class
*/
class MySQLDatabase implements Database
{
protected $link;
public function connect($host, $user, $pass, $db)
{
$this->link = mysql_connect($host, $user, $pass);
// error handling
mysql_select_db($db, $this->link);
// error handling
}
public function query($query, array $parameters = array())
{
$query = vsprintf(str_replace('?', '%s', $query), $parameters);
return $this->fetch(mysql_query($query, $this->link));
}
public function fetch($result)
{
return mysql_fetch_assoc($result);
}
}
/**
* The PDO superclass
*/
abstract class PDODatabase implements Database
{
protected $link;
public function query($query, array $parameters = array())
{
$stmt = $this->link->prepare($query);
return $this->fetch($stmt->execute($parameters));
}
public function fetch($result)
{
return $result->fetch(PDO::FETCH_ASSOC);
}
}
/**
* The MySQL PDO class
*/
class PDOMySQLDatabase extends PDODatabase
{
public function connect($host, $user, $pass, $db)
{
$this->link = new PDO('mysql:host='.$host.';dbname='.$db, $user, $pass);
// error handling
}
}
/**
* The Oracle PDO class
*/
class PDOOracleDatabase extends PDODatabase
{
public function connect($host, $user, $pass, $db)
{
// maak connectie op de Oracle manier
$this->link = new PDO(...);
// error handling
}
}
Gewijzigd op 17/07/2012 18:14:50 door Wouter J
UserMapper
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
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
<?php
require 'mapper.php';
/**
* Class for communicating between the user and database object
*
* @author Roel van de Water
*/
class UserMapper extends Mapper
{
public function getByUsernameAndPassword($username, $password)
{
$query = "SELECT id, username, password, ip FROM users WHERE username = :username AND password = :password";
$result = $this->db->query($query, array(
':username' => $username,
':password' => $password
));
if ($this->db->num_rows($result) == 0)
{
return false;
}
else
{
$record = $this->db->fetch($result);
return $this->populate($record);
}
}
// overige methods
public function populate(array $data)
{
$user = new User($data['username'], $data['password'], $data['ip']);
$user->setId($data['id']);
return $user;
}
}
?>
require 'mapper.php';
/**
* Class for communicating between the user and database object
*
* @author Roel van de Water
*/
class UserMapper extends Mapper
{
public function getByUsernameAndPassword($username, $password)
{
$query = "SELECT id, username, password, ip FROM users WHERE username = :username AND password = :password";
$result = $this->db->query($query, array(
':username' => $username,
':password' => $password
));
if ($this->db->num_rows($result) == 0)
{
return false;
}
else
{
$record = $this->db->fetch($result);
return $this->populate($record);
}
}
// overige methods
public function populate(array $data)
{
$user = new User($data['username'], $data['password'], $data['ip']);
$user->setId($data['id']);
return $user;
}
}
?>
Database
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
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
<?php
/**
* Database object for communicating between the mapper and the PDO database object
*
* @author Roel van de Water
*/
class Database
{
protected $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;;
}
public function query($query, array $args = null)
{
try
{
$statement = $this->pdo->prepare($query);
$statement->execute($args);
return $statement;
}
catch (PDOException $e)
{
throw new Exception('Query failed');
}
}
public function lastInsertId()
{
return $this->pdo->lastInsertId;
}
public function num_rows($result)
{
return count($result->fetchAll(PDO::FETCH_ASSOC));
}
public function fetch($result)
{
return $result->fetch(PDO::FETCH_ASSOC);
}
}
?>
/**
* Database object for communicating between the mapper and the PDO database object
*
* @author Roel van de Water
*/
class Database
{
protected $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;;
}
public function query($query, array $args = null)
{
try
{
$statement = $this->pdo->prepare($query);
$statement->execute($args);
return $statement;
}
catch (PDOException $e)
{
throw new Exception('Query failed');
}
}
public function lastInsertId()
{
return $this->pdo->lastInsertId;
}
public function num_rows($result)
{
return count($result->fetchAll(PDO::FETCH_ASSOC));
}
public function fetch($result)
{
return $result->fetch(PDO::FETCH_ASSOC);
}
}
?>
Als ik nu probeer in te loggen krijg ik de volgende error:
Catchable fatal error: Argument 1 passed to UserMapper::populate() must be of the type array, boolean given, called in C:\wamp\www\RoelVanDeWater\classes\usermapper.php on line 27 and defined in C:\wamp\www\RoelVanDeWater\classes\usermapper.php on line 65
Als ik op regel 38 van Database instel dat er altijd 1 gereturned wordt doet ie het wel, maar dat klopt natuurlijk niet. Ik trek zowat de haren uit m'n hoofd want ik snap gewoon niet wat er verkeerd gaat...
Oplossing: fetch all records direct na het uitvoeren van je query, sla het in je DB class op (of in een andere class die met resultaten om kan gaan) en doe alle bewerkingen op die opgeslagen set.
Dat vermoedde ik ook al. Maar is dat wel een nette oplossing? Je kunt dan namelijk niet meer met het statement doet wat je wilt buiten de database class.