Inleiding
Schreef ik in een eerdere post nog over het ontbreken van een beheergedeelte voor mijn zelfgebouwde weblog, is dat intussen hard in de maak. Ik heb ervoor gekozen om het beheer van dit weblog via de commandline (CLI) te doen. Dit is voor mij een nieuw terrein; PHP, maar dan buiten een webbrowser om.
Script
De eerste regel van het script vertelt de shell, na een hekje en een
uitroepteken, welk programma gebruikt moet worden om de rest van het bestand uit
te voeren. Dit heet in de Unix-volksmond een shebang. Daarna begint de
php
-code.
Als eerste wordt de functie main
gedefinieerd, en daarna in een while (true)
{ }
-loop opgeroepen. In deze functie wordt het scherm leeg gemaakt (clear
),
alle posts ingelezen (BlogPost::readFromDisk()
) en aan de gebruiker gevraagd
een keuze te maken. Op basis van die keuze wordt er één van de volgende acties
uitgevoerd:
- Nieuwe blogpost aanmaken
- Bestaande blogpost bewerken
- Bestaande blogpost verwijderen
- Programma afsluiten
#!/usr/bin/php
<?php
function main()
{
system('clear');
$blogs = FW\BlogPost::readFromDisk(BLOGPATH);
printTable($blogs);
$blogcount = count($blogs);
$prompt = sprintf(
" Maak uw keuze [n]ew, [e]dit, [d]elete, [q]uit: ", $blogcount
);
$line = '';
while (empty($line)) {
$line = readline($prompt);
}
switch (trim(strtolower($line)))
{
case 'n':
newPost($blogs);
break;
case 'e':
editPost($blogs);
break;
case 'd':
deletePost($blogs);
break;
case 'q':
print PHP_EOL;
exit;
break;
}
return;
}
while (true) {
main();
}
Nieuwe blogpost
Als eerste wordt de gebruikter gevraagd een titel op te geven. Daarna volgt een
voorstel voor de SEO-titel; de tekenreeks die in de adresbalk achter /blog/
komt te staan. Dit voorstel wordt afgeleid van de opgegeven titel. Tot slot
volgen nog naam en e-mailadres van de auteur, waarna de editor wordt gestart met
het nieuwe .markdown
-document. Is de editor gesloten, dan wordt het
XML-bestand met metadata weggeschreven.
function newPost(&$blogs)
{
$prompt = sprintf(" Titel: ");
$newTitle = '';
while (empty(trim($newTitle))) {
$newTitle = readline($prompt);
if (postExists($blogs, null, $newTitle)) {
print " FOUT! Titel bestaat al. ".PHP_EOL;
$newTitle = '';
}
}
$proposalSeoTitle = F\Functions::getSeoString($newTitle);
$prompt = sprintf(" SEO-titel [%s]: ", $proposalSeoTitle);
$newSeoTitle = '';
while (empty($newSeoTitle)) {
$newSeoTitle = readline($prompt);
if (empty($newSeoTitle)) {
$newSeoTitle = $proposalSeoTitle;
} else {
$newSeoTitle = F\Functions::getSeoString($newSeoTitle);
}
if (postExists($blogs, null, null, $newSeoTitle)) {
print " FOUT! SEO-titel bestaat al. ".PHP_EOL;
$newSeoTitle = '';
}
}
$proposalAuthorName = DEFAULT_AUTHOR_NAME;
$prompt = sprintf(" Auteur [%s]: ", $proposalAuthorName);
$newAuthorName = readline($prompt);
if (empty($newAuthorName)) {
$newAuthorName = $proposalAuthorName;
}
$proposalAuthorEmail = DEFAULT_AUTHOR_EMAIL;
$prompt = sprintf(" E-mail [%s]: ", $proposalAuthorEmail);
$newAuthorEmail = '';
while (empty($newAuthorEmail)) {
$newAuthorEmail = readline($prompt);
if (!empty($newAuthorEmail)
&& false === filter_var($newAuthorEmail, FILTER_VALIDATE_EMAIL)
) {
print " FOUT! Geen geldig e-mailadres. ".PHP_EOL;
$newAuthorEmail = '';
} else if (empty($newAuthorEmail)) {
$newAuthorEmail = $proposalAuthorEmail;
}
}
$bp = new FW\BlogPost($newTitle, $newSeoTitle);
$newFile = BLOGPATH.$bp->getID().'.markdown';
file_put_contents($newFile, '## Inleiding');
print " Starting editor...";
$cmd = EDITOR.' '.$newFile.' 1>&2 2>/dev/null;';
$editOK = -1;
system($cmd, $editOK);
if ($editOK === 0) {
$bpe = new FW\BlogPostEdit($newAuthorName, $newAuthorEmail);
$bp->addEdit($bpe);
$newFile = BLOGPATH.$bp->getID().'.xml';
file_put_contents($newFile, $bp->toXML());
}
return;
}
Bewerken blogpost
Na de keuze van de gebruiker (niet hieronder weergegeven) wordt er een MD5-hash
berekend van de huidige metadata van de blogpost (md5($bp->toXML())
). Deze
wordt later gebruikt om te controleren of er iets gewijzigd is.
Daarna worden, net als bij een nieuw blogpost, titel, seo-titel, auteur-naam en
-emailadres gevraagd. Tot slot wordt ook van het .markdown
-bestand een
MD5-hash berekend - en na het bewerken vergeleken met de nieuwe hash.
Als ofwel de metadata (het XML-bestand) of de inhoud (het Markdown-bestand) is
gewijzigd, wordt er een <edit>
-node aan de metadata toegevoegd en
weggeschreven.
function editPost(&$blogs)
{
// ...
$bp = $blogs[$indexToEdit];
$oldXMLhash = md5($bp->toXML());
$prompt = sprintf(" Titel [%s]: ", $bp->getTitle());
$newTitle = '';
while (empty($newTitle)) {
// ...
}
$prompt = sprintf(" SEO-titel [%s]: ", $bp->getSeoTitle());
$newSeoTitle = '';
while (empty($newSeoTitle)) {
// ...
}
$proposalAuthorName = DEFAULT_AUTHOR_NAME;
$prompt = sprintf(" Auteur [%s]: ", $proposalAuthorName);
$newAuthorName = readline($prompt);
// ...
$proposalAuthorEmail = DEFAULT_AUTHOR_EMAIL;
$prompt = sprintf(" E-mail [%s]: ", $proposalAuthorEmail);
$newAuthorEmail = '';
while (empty($newAuthorEmail)) {
// ...
}
$bp->setTitle($newTitle);
$bp->setSeoTitle($newSeoTitle);
$prompt = " Start editor? [Y/n]: ";
$line = readline($prompt);
if (empty($line)) {
$line = 'y';
}
$mdFile = BLOGPATH.$bp->getID().'.markdown';
$oldMDhash = md5_file($mdFile);
if (trim(strtolower($line)) == 'y') {
$editOK = -1;
print " Starting editor...";
$cmd = EDITOR.' '.$mdFile.' 1>&2 2>/dev/null;';
system($cmd, $editOK);
}
$newMDhash = md5_file($mdFile);
$newXMLhash = md5($bp->toXML());
if ($newXMLhash != $oldXMLhash || $newMDhash != $oldMDhash) {
$bpe = new FW\BlogPostEdit($newAuthorName, $newAuthorEmail);
$bp->addEdit($bpe);
$xmlFile = BLOGPATH.$bp->getID().'.xml';
file_put_contents($xmlFile, $bp->toXML());
}
return;
}
Verwijderen blogpost
Mijn moeder zei vroeger: Was'te voet wurps', bis'te kwiet
, wat zoveel
betekent als: als je iets weggooit, ben je het kwijt. Dit gaat zeker op voor
digitale inhoud zoals blogposts. Daarom heb ik ervoor gekozen om de XML- en
Markdown-bestanden niet te verwijderen, maar te hernoemen. Zo verdwijnen ze voor
de gebruiker uit het zicht, maar kunnen ze (bijvoorbeeld via FTP) moeiteloos
worden 'teruggetoverd'.
function deletePost(&$blogs)
{
// ...
$bp = $blogs[$indexToDelete];
$prompt = sprintf(
" Weet u zeker dat u \"%s\" wil verwijderen? [y/N]: ",
$bp->getTitle()
);
$line = '';
while (empty($line)) {
$line = readline($prompt);
}
if (trim(strtolower($line)) == 'y') {
$oldFile = BLOGPATH.$bp->getID().'.xml';
$newFile = BLOGPATH.$bp->getID().'.xml.deleted';
rename($oldFile, $newFile);
$oldFile = BLOGPATH.$bp->getID().'.markdown';
$newFile = BLOGPATH.$bp->getID().'.markdown.deleted';
rename($oldFile, $newFile);
}
return;
}
Foutafhandeling
Bij het verwerken van invoer van gebruiker hoort altijd een gezonde dosis wantrouwen. Soms moet je als ontwikkelaar ook de gebruiker een handje helpen om te zorgen dat hij/zij er geen puinhoop van maakt - of kan maken.
Duplicaten
while (empty(trim($newTitle))) {
$newTitle = readline($prompt);
if (postExists($blogs, null, $newTitle)) {
print " FOUT! Titel bestaat al. ".PHP_EOL;
$newTitle = '';
}
}
Bovenstaand stuk code controleert tijdens het invoeren van een nieuwe titel of er al een blogpost met die titel bestaat. Deze logica heb ik centraal gelegd, zodat ze ook kan worden hergebruikt bij SEO-titel:
function postExists(&$blogs, $idToExclude = null, $title = null, $seoTitle = null)
{
if (empty($title) && empty($seoTitle)) {
return false;
}
$found = array_filter(
$blogs,
function ($bp) use ($idToExclude, $title, $seoTitle) {
return
(empty($idToExclude) || $bp->getID() != $idToExclude) &&
(empty($title) || $bp->getTitle() == $title) &&
(empty($seoTitle) || $bp->getSeoTitle() == $seoTitle);
}
);
return count($found) > 0;
}
E-mailadres
Schreef en hergebruikte ik vroeger nog handmatig een RegEx voor het valideren
van e-mailadressen, neemt PHP mij dit werk vanaf versie 5.2 met
filter_var
dankbaar uit handen:
if (!empty($newAuthorEmail)
&& false === filter_var($newAuthorEmail, FILTER_VALIDATE_EMAIL)
) {
print " FOUT! Geen geldig e-mailadres. ".PHP_EOL;
$newAuthorEmail = '';
}