Spring naar hoofdtekst

Weblog beheer via commandline PHP

Geplaatst op door ,
Laatste aanpassing op .

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 = '';
}

Inhoudsopgave

Klik op één van de onderstaande categorieën om de lijst met artikelen te filteren.