dinsdag 15 juli 2008

Deel 9: Een groepsnaam wijzigen

Dit keer gaan we de groepen afwerken. Ik wil twee icoontjes bij elke groep plaatsen één voor wijzigen en één voor verwijderen van een groep.
Als voorbeeld gebruik ik de bake tool om een werkend voorbeeld te hebben van de CRUD (Create, Read, Update, Delete) functies.

Eerst maar eens een link per groep om deze te kunnen wijzigen en te verwijderen. Ik heb ervoor gekozen een icoontje voor de groepsnaam te plaatsen en zo de actie te triggeren. Daarvoor kunnen we de table functies gebruiken die deel zijn van de html helper.
Kies daarom leuk kleine plaatjes om als link naar de edit- en addactie te gaan dienen en plaats het in webroot/img.

Open views/groups/index.ctp.
Om een link van een plaatje te maken moeten een combinatie van html->link en html->img gebruiken:

$cells = array();
foreach($groups as $groupSet):
foreach($groupSet as $k => $group):
array_push($cells, array(
// edit link voor deze groep
$html->link(
$html->image('/img/edit.png', array('alt' => 'Edit groepnaam', 'border'=>'0')),
'/groups/edit/'.$group["id"],
array(), false, false
),
// delete link voor deze groep
$html->link(
$html->image('/img/delete.png', array('alt' => 'verwijder groep', 'border'=>'0')),
'/groups/delete/'.$group["id"],
array(), false, false
),
// doorklikken naar de links van deze groep
$html->link($group["naam"],array("controller"=>"Links", "action"=>"index/".$group["id"]."?groepsnaam=".urlencode($group["naam"])))
));
endforeach;
endforeach;
// tabelletje op het scherm zetten
echo "<table>";
echo $html->tableCells($cells);
echo "</table>";

Het gebruik van deze helpers zorgt ervoor dat altijd geldige html wordt gegenereerd. De tabel zorgt dat de icoontjes netjes naast de groepnamen komen te staan.
De helper $html->tableCells() maakt van een array een serie TR's en TD's:
$html->tableCells(array(array(1,2,3), array(4,5,6), array(7,8,9)));
genereert de volgende code:

<tr><td>1</td> <td>2</td> <td>3</td></tr>
<tr><td>4</td> <td>5</td> <td>6</td></tr>
<tr><td>7</td> <td>8</td> <td>9</td></tr>

Daar hoeft dus alleen nog een table omheen. De actie voor een nieuwe groep zullen we ook gelijk aanpassen, dat wordt verderop gebruikt:
link("Nieuwe groep", array("controller"=>"Groups", "action"=>"add"));?>
Vraag het scherm op via de webinterface om te zien hoe de groepen er nu uitzien.



Nu de view in orde is, hebben we de controller nodig.
Hernoem de huidige group_controller.php in controllers om te voorkomen dat ie overschreven wordt.
Open een DOS box en ga naar je programma directory. Daar type je
cake bake
om de utility te openen.
Kies Controller -> Group,
- interactive: n
- include basic class methods: y
- admin routing: n
- Look Okay?: y
- unit test files: n
In de controllers dir is nu een nieuwe groups_controller.php gemaakt met daarin een method voor add() edit() en delete() (en nog wel wat meer, maar dat hebben we niet nodig). Hernoem groups_controller.php naar groups_controller_baked.php, en herstel de vorige groups_controller.php in de originele staat.
Nu eerst een klein uitstapje. In de controller wordt gecontroleerd of de gebruiker ingelogd is. Na dat geconstateerd te hebben kunnen we eigenlijk gelijk de layout wijzigen naar "ingelogd". Open het oorspronkelijke groups_controller bestand. Kopieer
$this->layout = "loggedin";
uit de index() functie naar de __validateLoginStatus() functie, en wel als we zeker weten dat er is ingelogd:

function __validateLoginStatus(){
if($this->action != 'login' && $this->action != 'logout'){
if(!$this->Session->check('User')){
$this->Session->setFlash('Om deze pagina te bekijken moet je eerst inloggen');
$this->redirect('/users/login');
}
$this->layout = "loggedin";
}
}

In andere methods is dit nu dus niet meer nodig.
Nu de edit() en delete() functies en nu we toch bezig zijn gaan we de add() functie refactoren, naar voorbeeld van de bake actie.
Er is maar een kleine aanpassing nodig:

$user = $this->Session->read('User');
$this->set("user_id", $user['id']);

Dit komt in plaats van de koppeling van de user die bake heeft gemaakt. Verder kun je de methods add() edit() en delete() kopiëren. De functie nieuwe_groep() is niet meer nodig en mag opgeruimd worden. Rest ons nog om twee views toe te voegen, 1 voor edit en een voor add. Deze views zijn precies hetzelfde als de bestaande view nieuwe_groep.ctp. Hernoem dat bestand naar edit.ctp en maak er een kopie van die je add.ctp noemt. De code zelf hoeft niet aangepast te worden. Let op de forms die op je scherm komen, ze posten automatisch naar de juiste controller-action.

He group-gedeelte is nu af. (dwz, alles werkt). Het was sneller geweest direct de bake utility te gebruiken, maar nu heb ik veel meer geleerd. Het onderhouden van de links in groepen zal nu niet veel problemen meer opleveren. Ik heb nog even de favicon veranderd (in /webroot) hier is de code tot nu toe.

maandag 14 juli 2008

Deel 8: Nieuwe groep aanmaken

Om een nieuwe groep aan te maken moeten we weten voor welke user. Als dat meegestuurd wordt in de POST kan het Cake DBO de nieuwe entry opslaan.
Eerst maar eens een link om de aktie om een nieuwe groep aan te maken te starten:
open views/groups/index.ctp en voeg deze regel toe:

<?php echo $html->link("Nieuwe groep", array("controller"=>"Groups", "action"=>"nieuweGroep"));?>

We kunnen nu de te nemen stappen door CAKE laten melden in debus informatie.
Start de groepen pagina en klik de "Nieuwe groep" link. Cake meldt nu dat er een method nieuweGroep() wordt verwacht in de controller GroupsController. Het skelet van de functie wordt meegegeven. Neem dus deze functie op in groups_controller.php:

/**
* Een nieuwe groep creeren
* @return
*/
function nieuweGroep() {
$user = $this->Session->read('User');
$this->set("user_id", $user['id']);
$this->layout = "loggedin";
}

De userID zit in de sessie, en die hebben we nodig. Verder gebruiken we de layout loggedin om te zorgen dat het menu verschijnt, inclusief de uitleggen link.
Druk op F5 om te refreshen en nu blijkt dat we een view missen: \views\groups\nieuwe_groep.ctp
Maak het gevraagde bestand aan. Daar moet een invulformulier op komen.

<h3>Nieuwe groep</h3>
<div id="formwrapper">
<?php echo $form->create(); ?>
<?php echo $form->input("naam", array("label"=>"Groepnaam"));?>
<?php echo $form->input("users_id", array("type"=>"hidden", "value"=> $user_id)); ?>
<?php echo $form->end("Opslaan"); ?>
</div>

Ik gebruik weer de formwrapper DIV om de uitlijning van de velden te regelen.
Verder verschijnt het hidden veld met de userID die doorgegeven wordt door de controller. Als we naar de bron van de pagina kijken, blijkt het formulier te posten naar groups/add. De controller verwacht dus een add() method in de groups-controller.
We maken deze method aan in groups_controller.php met de volgende body:
 
/**
* opslaan van een nieuwe groep
* @return
*/
function add(){
if(!empty($this->data)){
if($this->Group->save($this->data)){
$this->Session->setFlash("Groep opgeslagen");
} else {
$this->Session->setFlash("Fout bij opslaan van groep");
}
}
$this->redirect("/groups/index");
}

Nu kun je een nieuwe groep aanmaken in de database.

vrijdag 4 juli 2008

Tussendeel: een stukje refactoren

Hoe meer ik lees over CakePHP hoe meer ik zie dat ik wat te veel heb overgeslagen. Nu dan maar even een kort stukje refactoring.

Om beter gebruik te maken van de form-helper en eindelijk een einde te maken aan die tables die een form zo groot maken, heb ik maar eens gekeken of ik een standaard manier kan vinden om het uitlijn effect van een table zo goed mogelijk na te bootsen in CSS. Een bijkomend voordeel is dat ik de form helper veel meer kan laten genereren en de code dus compacter wordt.
Na lezen over de form-helper in de docs (rtfm) heb ik het inlog formulier teruggebracht naar het volgende:
\views\users\login.ctp

<h2>Inloggen</h2>
<div id="formwrapper">
<?php echo $form->create('User', array('action' => 'login'));?>
<?php echo $form->input("username");?>
<?php echo $form->input("password");?>
<?php echo $form->end("Login"); ?>
</div>

Het meeste wat hierboven staat is vast duidelijk. De inputs leveren een label op + het input veld, zoals hieronder:
<div class="input text">
<label for="UserUsername">Username</label>
<input name="data[User][username]" type="text" value="" id="UserUsername" />
</div>

De $form->end() levert een </form> op én een submit-button.
Om dat netjes uitgelijnd te krijgen heb ik wat meer CSS nodig:

\webroot\css\stijlen.css

#formwrapper{
width: 350px;
border: 1px solid black;
text-align: right;
}
div > label{
float:left;
}
input[type=text]{
width: 15em;
}
input[type=password]{
width: 15em;
}

De truuk is als volgt: de wrapper, die de beschikbare breedte voor het form bepaalt, daarbinnen rechts uitlijnen voor de input-velden en een float:left voor de labels. Werkt in elk geval in msie 7. ff 3 en opera 9. De vaste breedte van de wrapper kan nog een probleem opleveren: we'll cross that bridge when we get to it...
Verder hoeft niets te worden gewijzigd: wat een mooie scheiding van presetatie en business logica! Van 30 naar 7 regels in 5 minuten...

dinsdag 1 juli 2008

Deel 7: Groepen links opvragen

De volgende stap is het ophalen van de link groepen van een user.
De groepen staan in de groups tabel. Er is een 1:n relatie met de users tabel of, in CakePHP termen:
een group belongs-to een user
Dit is gemodelleerd in een FK relatie van group naar user. In het model wordt dit direct aangegeven, wat als voordeel heeft dat bij een query naar de groupen ook de users gegevens worden opgehaald. Nadeel s de overhead.

Open een dos-box en cd naar je scolafavs dir. Type
cake bake
en kies het maken van een model.
Kies ervoor een model association aan te geven. Cake ziet dat het waarschijnlijk om de users tabel gaat.

---------------------------------------------------------------
Possible Models based on your current database:
1. Group
2. Link
3. User
Enter a number from the list above, type in the name of another model, or 'q' to exit
[q] > 1
Would you like to supply validation criteria for the fields in your model? (y/n)
[y] > n
Would you like to define model associations (hasMany, hasOne, belongsTo, etc.)? (y/n)
[y] > y
One moment while the associations are detected.
---------------------------------------------------------------
Please confirm the following associations:
---------------------------------------------------------------
Group belongsTo Users? (y/n)
[y] > y
Would you like to define some additional model associations? (y/n)
[n] > n

Open het gegenereerde bestand en voeg de volgende method toe:

/**
* Haal de groepen van een user op
* @return
* @param $data Object
*/
function getMyGroups($data){
$groups = $this->findAllByUsersId($data['user_id']);
if(!empty($groups)) {
return $groups;
}
return false;
}

Heel eenvoudig en portable. 1 method om de linkgroepen van een user op te halen. De findAllBy... method wordt door het CakePHP model verzorgd.

Nu de controller. Maak een bestand groups_controller.php in /controllers en neem de volgende index() method op:

/**
* Haal standaard de link groepen van een user op
* @return
*/
function index(){
$this->layout = "loggedin";
$data["user_id"] = $this->Session->read('User');
// haal de groepen van deze user op
$this->set("groups", $this->Group->getMyGroups($data));
}


De gebruiker moet ook nog naar de pagina kunnen navigeren. Daarom hebben we een aantal links nodig, die pas na inloggen getoond moeten worden. Daarvoor is de regel
$this->layout = "loggedin";
Deze regel zorgt ervoor dat we in plaats van de layout "default" nu de layout "loggedin" gaan tonen. Die moeten we dan wel hebben. Creëer het bestand loggedin.ctp onder /views/layouts. Ik heb er de volgende code in staan:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<?php echo $html->charset(); ?>
<title>ScolaFavs</title>
<?php
echo $html->css('stijlen');
echo $scripts_for_layout;
?>
</head>
<body>
<div id="container">
<div id='menu'>
<?php echo $html->link("Home", array('controller' => 'Users', 'index' => 'logout')); ?>
<?php echo $html->link("Mijn Groepen", array('controller' => 'Groups', 'action' => 'index')); ?>
<?php echo $html->link('Logout', array('controller' => 'Users', 'action' => 'logout')); ?>
</div>
<div id="content">
<?php
if ($session->check('Message.flash')):
$session->flash();
endif;
echo $content_for_layout;
?>
</div>
</div>
<?php echo $cakeDebug?>
</body>
</html>


Nu nog de view voor de linkgroepen. Maak een bestand index.ctp aan onder /views/groups en vul 'm bijvoorbeeld zo:

<h3>Mijn Groepen!</h3>
<?php
foreach($groups as $groupSet){
foreach($groupSet as $k => $group){
if($k == "Group"){
echo $html->link(">> " . $group["naam"],"/groups/getLinks/".$group["id"]) . "<br/>";
}
}
}
?>


En tenslotte breiden we de onbeveiligde homepage uit met een link naar de inlogpagina:
views/pages/home.ctp

<h3>Welkom bij ScolaFavs!</h3>
<br/>
klik hier om in te loggen
<br/>
<?php
echo $html->link("Inloggen", array("controller"=>"Users", "action"=>"login"));
?>
<br/>
Meer informatie over deze applicatie kun je hier lezen...

Als alles goed is gegaan kun je nu via de bekende URL de applicatie starten en, na inloggen, je groepen opvragen.

Een ding hebben we overgeslagen: de groups_controller is niet beveiligd. Beveiligen is echter een kwestie van vooraf bekijken of de sessie een user bevat. hoewel dit wel een beetje inbraakgevoelig is, houden we het hier even op.

Neem deze twee functies op in de groups_controller om de controller te beveiligen:

function beforeFilter() {
$this->__validateLoginStatus();
}

function __validateLoginStatus(){
if($this->action != 'login' && $this->action != 'logout'){
if(!$this->Session->check('User')){
$this->Session->setFlash('Om deze pagina te bekijken moet je eerst inloggen');
$this->redirect('/users/login');
}
}
}

We gebruiken dus de code in de users_controller als nog niet is ingelogd door een forward naar de users controller en daarvan de login actie. Als er wel is ingelogd doet de validatie-method verder niets.