Hoi allemaal!

Ik ben weer eens lekker aan het programmeren, en dit keer ook weer OOP-stijl. Ik was daarom van plan om een heel gebruikerssysteem te maken met OOP. Ik heb veel gehad aan de uitleg van Wouter J, en dan bedoel ik deze post: klik

Het probleem waar ik op vastloop is het volgende. Wanneer je een gebruikerssysteem hebt, dan is het natuurlijk logisch dat gebruikers weer inloggen en dat ze dan een object hebben met hun gegevens daarin.

Nu is de vraag, wát moet daar precies in, en welke methods gebruik je hiervoor? Moet je bijvoorbeeld ook gegevens als een registratiedatum, laatst actief e.d. in het object bewaren (zelfs als je ze niet gebruikt)?

M'n tweede punt is hoe je zo'n bestaande gebruiker weer terug omzet in object. Als je bij het voorbeeld van Wouter J de constructor gebruikt met een paar argumenten zoals de gebruikersnaam en het wachtwoord, dan kun je deze niet meer gebruiken om een nieuw object aan te maken waarin we gelijk alle gegevens (zoals registratiedatum) meesturen. Hoe los ik dat op? Uiteraard gebruiken we een mapper voor de communicatie tussen het gebruikersobject en de database.

Dit leek me wel een leuk discussiepunt en een leerzaam topic. Althans, voor mij :-)

Roel
Roel op 12/06/2012 23:18:24
Nu is de vraag, wát moet daar precies in, en welke methods gebruik je hiervoor? Moet je bijvoorbeeld ook gegevens als een registratiedatum, laatst actief e.d. in het object bewaren (zelfs als je ze niet gebruikt)?

Als het onderdeel is van het object 'gebruiker' dan ja. Alleen hoef je het niet altijd te laden natuurlijk. Op de ene pagina heb je wel de registratiedatum nodig, op de andere niet. Bij mij past alles in de class (als ik zo'n class zou hebben, maar dat terzijde), maar bepaalt 'het model' (de classes die de database besturen) welke data er wordt opgehaald. De gebruikers class heeft dus de mogelijkheid om alles op te slaan en te verwerken, maar het is niet noodzakelijk.

Roel op 12/06/2012 23:18:24
M'n tweede punt is hoe je zo'n bestaande gebruiker weer terug omzet in object. Als je bij het voorbeeld van Wouter J de constructor gebruikt met een paar argumenten zoals de gebruikersnaam en het wachtwoord, dan kun je deze niet meer gebruiken om een nieuw object aan te maken waarin we gelijk alle gegevens (zoals registratiedatum) meesturen. Hoe los ik dat op? Uiteraard gebruiken we een mapper voor de communicatie tussen het gebruikersobject en de database.

Gebruik een array om de gegevens mee te geven aan de class. Gegevens uit de database krijg je normaal gesproken in een array, dus het is vrij simpel om die mee te geven in de gebruikers class. In de gebruikers class kan je die dan ook als array opslaan. Voor het uitlezen ervan kan je dan kiezen voor of specifieke methodes per veld, of je gebruikt de magische getter (__get()) waardoor je vooraf niet eens hoeft te weten welke velden precies allemaal in de class kunnen zitten. Dit laatste is niet altijd aan te raden, want je verliest daarmee wel de mogelijkheid om de methodes in slimme editors in de code hints te zien.

Goed punt over de array, in een database werkt het inderdaad ook zo.
Hoe zie jij een gebruikersobject dan voor je? Een object met slecht één array en een __get() en __set() method?
Dat ligt natuurlijk aan de functionaliteit die je in die classe wilt hebben.
Wat ik heb is een resultset class die de gegevens uit de database beheert en daar waar gevraagd beschikbaar stelt, inclusief de juiste formatting van bijvoorbeeld getallen en datums. Deze resultset class werkt dan weer met specifieke classes die de 'kennis' hebben van de data die wordt gebruikt om de formatting goed te doen. Zo heb ik een extra class voor users, voor landen, voor plaatsen etc. Iedere class weet wat er moet gebeuren met de data om het op een juiste manier te tonen. De resultset class bewaart het alleen maar en is in feite dom.

In jouw geval gaat het er dus om dat je moet bepalen of je gebruikersclass ook daadwerkelijk functionaliteit moet toevoegen of niet. Moet het iets kunnen doen als er een bepaalde waarde van een gebruiker wordt veranderd bijvoorbeeld? Zo niet, dan kan je waarschijnlijk volstaan met een zeer klein aantal methodes. Een __construct() om de data mee te geven en een __get() om het eruit te halen, met mogelijk nog een __call() als je bij het ophalen nog extra info wil meegeven (bijvoorbeeld een default waarde als de waarde niet bestaat). Ik heb er ook nog een exists() bij zodat andere classes kunnen checken of de waarde bestaat of niet.
Het properste is nog altijd om een object zo op te maken, zodat je op voorhand de parameters van de class kent. Vervolgens moet je voor elke functie een get en set functie maken. Dit kan inderdaad wel veel tijd innemen, maar is wel het overzichtelijkste.

Vervolgens kan je in de mapper een fromArray en een toArray functie maken die dan het object van en naar een array omzet om op te slaan of op te halen van een database.

Verder ben ik ook fan van een service layer en een mapper layer te maken.
Vb: http://i47.tinypic.com/2unvts4.png
Hier dienen de mappers ENKEL voor het omzetten van object en database.
De Table (adapter) layer dient dan voor het uitvoeren van de database instructies
De service layer dient dan voor het opslaan/ophalen/verwijderen van gegevens. Deze roept ook steeds de mapper op om de gegevens om te zetten naar de juiste objecten. Je roept in de Models/controllers dus enkel de service layer aan.

Natuurlijk is dit wel tijdsintensief.

Wat Erwin aanhaalt is natuurlijk ook een oplossing. Dit heb ik zelf ook eens gebruikt. Ik had een abstract object waarin dan die array zat, de __get en __set. Ik heb ook nog een echte functie 'get' en 'set' toegevoegd. Dan ben ik nog iets verder gegaan en heb ik de ArrayAccess interface geimplementeerd (http://www.php.net/manual/en/class.arrayaccess.php) Hierdoor kan je dus een object als array gebruiken. Hierdoor kon ik een object op volgende manieren gebruiken:

<?php
$obj = new Product();
$product->id = 1;
$product['id'] = 1;
$product->set('id', 1);
$product->flushDescription();
Service_Product::save($product);
?>

Deze manier van werken bespaart natuurlijk veel tijd, maar is niet al te proper en geeft ook geen code completition in uw editor.


Ik ben het met VeeWee eens dat de manier die ik gebruik enigszins problemen kan opleveren op het moment dat je bijvoorbeeld tikfouten maakt in je code. Omdat de getters en setters niet allemaal gedefinieerd zijn wordt het nooit gecontroleerd. Heb je dus een property 'registratiedatum' en je tikt 'regstratiedatum', dan wordt dat gewoon geaccepteerd en je krijgt zelfs een waarde (de default lege string). Dat is zeker iets om rekening mee te houden. Ik heb het gedaan omdat ik 1 class wilde hebben waarmee ik willekeurig welke resulset uit mijn database mee kon afhandelen. Deze class geeft in feite de data door van het model naar de view en moet eenduidig zijn. Met behulp van extra classes (zoals boven beschreven) zorg ik voor een uitwisselbare controle op de interne data en formatting.
Ik ben vandaag bezig geweest met een gastenboek. Graag commentaar en verbeteringen op m'n script :-)

index.php
<?php
require 'classes/gastenboek.php';

$gastenboek = new Gastenboek(new PDO('mysql:host=localhost;dbname=test', 'root', ''), (isset($_GET['id']) ? $_GET['id'] : ''));

if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
    if (!isset($_POST['naam']) && $_POST['naam'] == '' || !isset($_POST['reactie']) || $_POST['reactie'] == '')
    {
        $resultaat = 'Je hebt niet beide velden ingevuld!';
    }
    else
    {
        $gastenboek->toevoegen(new Reactie($_POST['naam'], $_POST['reactie'], $_SERVER['REMOTE_ADDR']));
        $result = 'Je reactie is succesvol toegevoegd aan het gastenboek!';
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Gastenboek door Roel</title>
    </head>
    <body>
        <h1>Gastenboek door Roel</h1>
        <?php
        if (isset($result))
        {
            echo $result;
        }
        ?>
        <form method="post" action="">
            <p>
                Naam:<br />
                <input type="text" name="naam" />
            </p>
            <p>
                Reactie:<br />
                <textarea name="reactie" rows="6" cols="37"></textarea>
            </p>
            <p>
                <input type="submit" value="Reageren" /> <input type="reset" value="Herstel" />
            </p>
        </form>
        <h2>Geplaatste reacties</h2>
        <?php
        foreach ($gastenboek->getReacties() as $reactie)
        {
            echo '<p>Door <strong>'.$reactie->getNaam().'</strong> op <strong>'.$reactie->getDatum().'</strong></p>
                <p>'.$reactie->getReactie().'</p><hr />';
        }
        ?>
    </body>
</html>


classes/gastenboek.php
<?php
/**
 * Class voor een gastenboek
 *
 * @author Roel
 */

// Benodigde classes inladen
require 'reactiemapper.php';
require 'reactie.php';

class Gastenboek
{
    /**
     * De ReactieMapper om de communicatie tussen het Gastenboek object en de database te regelen
     * 
     * @var resource $_reactieMapper
     */
    private $_reactieMapper;
    
    /**
     * Het ID van het Gastenboek object
     * 
     * @var integer $_id
     */
    private $_id;
    
    /**
     * Constructor van een nieuw Gastenboek object
     * 
     * @param resource $pdo Het PDO databaseobject
     * @param integer $id Het ID van het Gastenboek - niet verplicht
     */
    public function __construct(PDO $pdo, $id = '')
    {
        $this->_reactieMapper = new ReactieMapper($pdo, $id);
        $this->_id = (int) $id;
    }
    
    /**
     * Het ophalen van alle reacties
     * 
     * @return array Een array met daarin de reacties
     */
    public function getReacties()
    {
        return $this->_reactieMapper->getReacties($this->_id);
    }
    
    /**
     * Het invoegen van een nieuwe reactie
     * 
     * @param resource $reactie Het Reactie object
     */
    public function toevoegen(Reactie $reactie)
    {
        $this->_reactieMapper->toevoegen($reactie);
    }
}
?>


classes/reactiemapper.php
<?php
/**
 * Class voor de communicatie tussen het Gastenboek object en de database
 *
 * @author Roel
 */

class ReactieMapper
{
    /**
     * Het PDO databaseobject
     * 
     * @var resource $_db 
     */
    private $_db;
    
    /**
     * Het ID van het Gastenboek waaraan de ReactieMapper gekoppeld is
     * 
     * @var integer $_gastenboekId 
     */
    private $_gastenboekId;
    
    /**
     * Constructor van een nieuw ReactieMapper object
     * 
     * @param resource $pdo Het PDO databaseobject
     * @param integer $gastenboekId Het ID van het Gastenboek
     */
    public function __construct($pdo, $gastenboekId)
    {
        $this->_db = $pdo;
        $this->_gastenboekId = (int) $gastenboekId;
    }
    
    /**
     * Het ophalen van alle reacties van het Gastenboek op te halen uit de database
     * 
     * @return array Een array met daarin de reacties 
     */
    public function getReacties()
    {
        // Lege array maken waarin de reacties komen te staan
        $reacties = array();
        
        // Query maken en uitvoeren
        $query = "SELECT naam, reactie, DATE_FORMAT(datum, '%d-%m-%Y om %H:%i') AS datum, ip FROM reacties WHERE gastenboek_id = :id";        
        
        $statement = $this->_db->prepare($query);
        
        $statement->execute(array(
            ':id' => $this->_gastenboekId
        ));
        
        // Alle reacties in de array plaatsen
        while ($record = $statement->fetch(PDO::FETCH_ASSOC))
        {
            $reacties[] = new Reactie($record['naam'], $record['reactie'], $record['ip'], $record['datum']);
        }
        
        // De array met daarin de reacties terugsturen naar het Gastenboek object
        return $reacties;
    }
    
    /**
     * Het toevoegen van een nieuwe reactie aan de database
     * 
     * @param resource $reactie Het Reactie object 
     */
    public function toevoegen(Reactie $reactie)
    {
        $query = "INSERT INTO reacties (gastenboek_id, naam, reactie, ip, datum) VALUES (:gastenboek_id, :naam, :reactie, :ip, NOW())";
        
        $statement = $this->_db->prepare($query);
        
        $statement->execute(array(
            ':gastenboek_id' => $this->_gastenboekId,
            ':naam' => $reactie->getNaam(),
            ':reactie' => $reactie->getReactie(),
            ':ip' => $_SERVER['REMOTE_ADDR']
        ));
    }
}
?>


classes/reactie.php
<?php
/**
 * Class voor een reactie in het gastenboek
 *
 * @author Roel
 */
class Reactie
{
    /**
     * De naam van degene die de reactie heeft geplaatst
     * 
     * @var string $_naam 
     */
    private $_naam;
    
    /**
     * De reactie
     * 
     * @var string $_reactie 
     */
    private $_reactie;
    
    /**
     * De datum van de reactie
     * 
     * @var string $_datum 
     */
    private $_datum;
    
    /**
     * Het IP-adres van degene die de reactie heeft geplaatst
     * 
     * @var string $_ip 
     */
    private $_ip;
    
    /**
     * Constructor voor een nieuw Reactie object
     * 
     * @param string $naam De naam van degene die de reactie geplaatst heeft
     * @param string $reactie De reactie
     * @param string $ip Het IP-adres van degene die de reactie geplaatst heeft
     * @param string $datum De datum van de reactie - niet verplicht wanneer de reactie nog geplaatst moet worden
     */
    public function __construct($naam, $reactie, $ip, $datum = '')
    {
        $this->_naam = (string) $naam;
        $this->_reactie = (string) $reactie;
        $this->_ip = (string) $ip;
        $this->_datum = (string) $datum;
    }
    
    /**
     * Het ophalen van de naam van degene die de reactie geplaatst heeft
     * 
     * @return string De naam van degene die de reactie geplaatst heeft
     */
    public function getNaam()
    {
        return $this->_naam;
    }
    
    /**
     * Het phalen van de reactie
     * 
     * @return string De reactie
     */
    public function getReactie()
    {
        return $this->_reactie;
    }
    
    /**
     * Het ophalen van de datum van de reactie
     * 
     * @return string De datum van de reactie 
     */
    public function getDatum()
    {
        return $this->_datum;
    }
}
?>


Ik vind hem zelf best goed gelukt. Wat vinden jullie ervan?
Ik vind het niet zo goed om bij elk object dat je maakt de database adapter mee te geven. Dat maakt het in een groter project onoverzichtelijk. Je moet normaal slechts 1 keer connectie maken met een database. Je hebt dit object enkel in de mapper nodig.
Mijn voorstel voor dit kleine projectje: maak een singleton. Bijvoorbeeld:
<?php
$this->_db = DatabaseAdapter::getInstance();
?>

Deze klasse kan je dan PDO laten extenden en zo dus gebruiken voor uw database connecties. zo blijven uw objecten overzichtelijker.


Ook het meegeven van alle parameters in de constructor zou ik zo niet doen. Je kan in de mapper een statische functie stoppen: fromArray(). Deze zou dan uw reactie object kunnen opbouwen uit form/database data. Zo kan je ergens anders in uw project die reactie gebruiken zonder dat je alle parameters moet invullen. In deze gastenboek is het natuurlijk niet nodig, maar in grotere projecten kan dat wel handig zijn.

Volgende stap is MVC ;)
if ($_SERVER['REQUEST_METHOD'] == 'POST')

veranderen naar:

if ($_SERVER['REQUEST_METHOD'] === 'POST')
@Bert: en waarom zou je dat willen doen?
@de VeeWee: dus een singleton is eigenlijk één grote(re) mapper voor meerdere objecten?
Verder bedankt voor je tips!

@Bert B: zou je inderdaad kunnen toelichten waarom?

Reageren