Soms heb ik enorm zin om dit soort code te kunnen schrijven:
<?php
$topic = $topic_store->get(24);
foreach($topic->posts() as $post)
echo $post->author()->nickname();
?>
Het probleem is dan: hoe komt $topic aan z'n posts? En hoe komen de posts aan een author record?
Topic is zeg maar zo'n class, volledig gevuld door Topic_Store via Topic::thaw($row):
<?php
class Topic
{
protected $data;
public function title()
{
return $this->data['title'];
}
public function posts()
{
return array(); // ???
}
public function freeze
{
return $this->data;
}
static public function thaw(array $data)
{
$topic = new self();
$topic->data = $data;
return $topic;
}
}
class Topic_Store
{
public function get($id)
{
$stmt = $this->pdo->prepare(...);
$row = $stmt->fetch();
return Topic::thaw($row);
}
}
?>
Voor Posts heb ik dan weer een aparte store, en een aparte record-class.
Hoe kan ik vanuit $topic bij de post_store om posts op te halen?
Register pattern is een mogelijkheid, maar ik vind het een anti-pattern.
Een overkoepelende class maken voor model, waar alle een link naar hebben, en die link door kunnen geven aan hun records zou een optie zijn, maar dan koppel je al die classes ook weer aan elkaar (al is daar iets voor te zeggen, het is immers je model, die gebruik je niet los van elkaar)
Hoe doen jullie dit koppelen? Wat vinden jullie de beste en de makkelijkste manier?
Over het verhaal van getters en setters ben ik met je eens, maar voor data-objecten gaat het volgens mij niet helemaal op.
Stel je gebruikt het volledige user-object zoals ik hem hierboven heb genomen, met een leger aan setters en getters. Die getters zijn nodig omdat het user-object niet alles wat betreft z'n data zelf kan. Ik denk niet dat een User zichzelf moet kunnen omzetten naar HTML, XML, RSS of ieder ander formaat waarin ik de data van de user wil gebruiken. Daar heb je weer aparte classes voor die dat kunnen "tekenen". Die classes moeten dan wel bij de nickname van de user kunnen.
Dan zou je kunnen gaan voor de meer struct-achtige aanpak, waarbij het meer een setje van variabelen is. Dit heeft als voordeel boven een array dat in ieder geval de structuur vast ligt. Als je een User object of struct hebt, dan weet je tenminste zeker dat $user->nickname bestaat. Maar, je kan niet garanderen dat de waarde die daarin zit geldig is.
En dan is het niet meer echt OOP programmeren. Het wordt allemaal heel plat en ik heb het gevoel dat de verantwoordelijkheden niet meer kloppen.
Maar toen ik dat typte, dacht ik, en misschien is dat ook wel wat jij bedoelde, waarom zo moeilijk doen? Het gaat om de interface naar buiten. Hoe die classes intern werken maakt niet zoveel uit. Dus: abstraction layer tussen de classes en de database (of een andere opslag, is ook prima mogelijk, maar alles wordt wel bij elkaar opgeslagen. En dat is juist een pluspunt)
Voordelen zijn dat je data redelijk goed alleen van binnen deze set classes aangepast kan worden (tenzij je een eigen Topic object instantieert, maar dat kan je in PHP niet tegenhouden. Gewoon niet doen)
Met die laatste code ben ik wel blij. Ik denk dat dat wel goed als uitgangspunt kan gebruiken.
De code die je hier in de pastebin geeft heeft geen enkele meerwaarde boven het gebruik van een simpele array met waarden, dus waar niet gewoon dat gebruiken dan? Het is sneller en werkt nog handiger ook, want je kan dan er nog overheen loopen.
Een klasse met met alleen maar attributen en getters en setters is dan ook geen OOP, je hebt alle voordelen van OOp compleet weggenomen. Wat ik dus zou doen is dus alle functionaliteit die iets moet met de waarden uit de User klasse daar ook daadwerkelijk plaatsen.
Het verdelen van de verantwoordelijkheden kan je vervolgens best doen, maar dan via een Strategy pattern. Je geeft dus bijv. aan de save method een UserMapper mee waaraan dan de waarden worden doorgegeven.
Op deze manier behoud het object de complete controle over zijn eigen data.
Ik zie je argument voor strategy pattern, maar ik vraag me af of het in alle gevallen een verbetering is boven getters. Bij insert queries wil ik, in ieder geval binnen deze classes, bij het id van een post of een user kunnen. En om een Post object te maken zou ik het ook wel prettig vinden direct daar het User object dat de auteur is aan te kunnen hangen.
Ik moet toch iets hebben waarmee ik een bepaalde user of een bepaalde post kan identificeren. Die informatie moet het tenminste vrij geven om iets aan hem te kunnen koppelen.
Voor de andere eigenschappen zou ik het me nog kunnen voorstellen:
<?php
interface UserPainter
{
public function paintUser($id, $nickname, $email);
}
class User
{
public function draw(UserPainter $painter)
{
return $painter->paintUser($this->id, $this->nickname, $this->email)
}
}
interface PostPainter
{
public function paintPost($id, User $author, $body);
}
class Post
{
public function draw(PostPainter $painter)
{
return $painter->paintPost($this->id, $this->author, $this->body);
}
}
class HTMLForumPainter implements UserPainter, PostPainter
{
public function paintUser(id, $nickname, $email)
{
return sprintf('<a href="/profiles/%d">%s</a>', $id, $nickname);
}
public function paintPost($id, User $author, $body)
{
return sprintf('<p>%s door %s</p>', $body, $author->draw($this));
}
}
// zal eerder hoger zijn, ergens rond Topic
echo $post->draw(new HTMLForumPainter);
?>
Maar of je er tijdens het programmeren voordeel aan hebt... Templates zijn niet langer praktisch, en je hebt redelijk wat classes nodig om alleen al het uiterlijk van je site te regelen.
edit: hoe dan bij het maken van een nieuw topic? Maak ik dan een object Topic aan (met titel-string als argument aan de constructor) en dan $topic->save($store)? Hoe dwing ik dan af dat er een openingspost meekomt? $topic->save($store, $openings_post)? Dat is maf, want afhankelijk van of $topic al eerder is opgeslagen moet je een extra parameter meegeven op niet de meest logische plek.
Of $topic = new Topic($title, array(new Post($user, $body))); $topic->save($store); ? Ook wat apart, maar zou nog gaan. Dat voor Post dezelfde store wordt gebruikt maakt niet uit, want dat zou sowieso al het geval zijn. Het is dan aan die store om ze met elkaar te koppelen (wss op basis van primary key)
Ik snap je gedachtengang niet helemaal, maar hoe ik het zou doen:
<?php
class Topic
{
private $id;
private $title;
private $author;
private $post;
public function __construct($id, $title, User $user, Post $post)
{
// Toewijzen
}
public function save(UserManager $manager)
{
// opslaan
}
public function weergeven(Template $tpl)
{
// Hier toekennen aan template
}
}
Hier kan nog wel het één en ander in geschoven worden. Zo kan je het UserManager object ook via de constructor geven en zou je ook een View klassen kunnen maken ipv een Template.
Overigens is dit nog steeds niet een hele handige methode, want het toevoegen van het topic en de reactie wil je altijd doen in een transactie voor je database. Het is dus mooier om ergens die transactie te abstraheren in een speciale klasse die je vervolgens gewoon gebruikt voor het opslaan van je gegevens.
Mijns inziens is het helemaal niet nodig om per see een user object te maken. Je kan ook gewoon een array met user data maken en die naar de template doorsturen, ophalen uit een UserManager object en ook opslaan met een UserManager object.
De validatie kan je vervolgens in je manager doen waar je gewoon een protected of private methode validate_user maakt die de array controleert.
Maar dit is een bekent probleem met relationele databases. Het idee is anders dan bij OOP. Je kan dus nooit tabellen rechtstreeks mappen aan objecten in je programma. Oplossing hiervoor is, gewoon die mapping niet doen of een object database gebruiken.