Wie dem ein oder anderen Leser hier sicherlich schon aufgefallen ist, gibt es auf
diginights.com seit geraumer Zeit nun eine
Gewinnspiel-Section. Die grundlegende Idee dahinter war, dem User mehr Funktionalität auf der Seite zu bieten, was uns mit dieser Idee (denke ich zumindest) auch ziemlich gut gelungen ist. Um automatische Eintragungen zu verhindern wurde ein
Captcha-Schutz eingebaut, der nun auf allen anderen Formularseiten Verwendung findet, da es in der Vergangenheit öfters vorkam, dass Spamer die Location-eintragen Seite für ihre Zwecke missbrauchten.
In diesem Beitrag will ich aber ein wenig über die Verlosung schreiben, bzw. die der zu Grunde liegenden Logik.
Es existieren drei von Propel generierte Klassen:
LotteryItem enthält die allgemeinen Infos zu einem Gewinnspiel, wie Name und Beschreibung oder das Auslosedatum, in
LotteryPrize finden sich die einzelnen Preise, die zu einem Gewinnspiel gewonnen werden können und
LotteryParticipant hält die Teilnehmer der jeweiligen Gewinnspiele. Um die Komplexität zu verringern sind die folgenden Codebeispiele auf das Nötigste gekürzt, zudem sind die PHPDoc Kommentare leider auch nicht dabei, da sie von meiner Blogsoftware in strong-Tags umgebaut werden (das muss ich mir nochmal anschauen):
1. Die naive Implementierung:
private function drawSingleLottery(LotteryItem $l) {
$prizes = $l->getLotteryPrizes();
// TODO: this is pretty slow and should be replaced with a piece of faster code in future
$participants = $l->getLotteryParticipants();
// determine winner array keys - exactly the amount of available prizes
$winnersKeys =
array_rand($participants,
$this->
countTotalWinnersForPrizes($prizes));
$winnersKeys =
array($winnersKeys);
}
$j = 0;
foreach ($prizes as $prize) { // loop all prizes
for ($i = 0; $i < $prize->getPrizeAmount(); $i++, $j++) {
$winner = $participants[$winnersKeys[$j]];
$winner->setLotteryPrize($prize);
}
}
$l->setIsDrawn(true);
$l->save();
}
Der Code läuft zwar, ist aber nicht schön: Die Methode
LotteryItem::getLotteryParticipants() liefert in einem Array sämtliche Objekte der Personen, die am Gewinnspiel teilgenommen haben. Desto mehr Teilnehmer es werden, desto langsamer wird es, da für jede Zeile des Datenbankresultats ein Objekt gebaut wird und die Instanziierung dessen eine teuere Operation ist.
Über
array_rand() werden genausoviele Schlüssel dieses Arrays zurückgegeben wie die in der selben Klasse definierte Methode
countTotalWinnersForPrizes() zurückgibt - sie zählt die gesamte Anzahl aller zu vergebenden Preise.
mixed array_rand ( array input [, int num_req] ) gibt ein Array mit den Schlüsseln zurück, wenn der 2. Parameter größer als 1 ist, ansonsten einen Schlüssel, also kein Array (als integer in unserem Falle), was ich an der Methodendefinition nicht besonders schön finde, da dieser Fall hier extra abgefragt werden muss.
Zum Schluss wird über die eigentlichen Preise iteriert, jedem Gewinner einen Preis zugeordnet und das Gewinnspiel gespeichert. Konstrukte wie
$participants[$winnersKeys[$j]]; sind unschön weil sie schwer zu lesen sind bzw. es schwer fällt, diese später wieder zu verstehen.
Ein
Refactoring muss also her ...
2. Die wesentlich eleganter-/performantere Implementierung:
private function drawSingleLottery(LotteryItem $l) {
$prizes = $l->getLotteryPrizes();
$winners = $l->determineWinners();
// set for every winner a prize
$winner_i = 0;
foreach ($prizes as $prize) { // loop all prizes
for ($i = 0; $i < $prize->getPrizeAmount(); $i++) {
$winner = $winners[$winner_i++];
$winner->setLotteryPrize($prize);
}
}
$l->setIsDrawn(true);
$l->save();
}
Alles, was im ersten Codefetzen hässlich war ist verschwunden und die neue Methode
LotteryItem::determineWinners() erblickt das Licht der Welt - diese scheint wohl direkt die Gewinner anstatt nur deren Keys zurückzuliefern, was nach einem Blick in den Code klar wird:
public function determineWinners() {
$amount = $this->countTotalWinnersForPrizes($this->getLotteryPrizes());
return LotteryParticipantPeer::retrieveRandomAmount($this, $amount);
}
Durch das Refactoring hat die
countTotalWinnersForPrizes() Methode die Klasse gewechselt, ihr Innenleben bleibt jedoch unverändert.
Abschließend interessiert nur noch die performantere Implementierung von
LotteryParticipantPeer::retrieveRandomAmount():
public
static function retrieveRandomAmount
(LotteryItem
$l,
$amount) { if (!
is_numeric($amount) ||
$amount <
1) throw
new Exception
('amount should be a positive integer');
$con = sfContext::getInstance()->getDatabaseConnection('propel');
$sql = 'SELECT '.self::ID.' FROM '.self::TABLE_NAME.' '.
'WHERE '.self::LOTTERY_ITEM_ID.' = '.$l->getId().' '.
'ORDER BY RAND() '.
'LIMIT '.$amount;
$q = new Query($con, $sql);
return self::retrieveByPKs($q->getCol(), $con);
}
...Diese baut lediglich eine Query von Hand mit dem in der aktuellen Propel-Version noch nicht abbildbaren MySQL-spezifischen
ORDER BY RAND(), das zufällig so viele Gewinner-IDs liefert, wie die Funktion als zweiten Parameter übergeben bekommt (MySQL
LIMIT). Das Resultat liefert über
LotteryParticipantPeer::retrieveByPKs() die gewünschten Objekte als Array zurück.
...So weit also mal ein kleiner Einblick in die Technik, die dahintersteckt - Ich habe euch hoffentlich nicht zu sehr gelangweilt