The Developer Fastlane

« 365 days to become a developer » challenge

PHP OOP: Guestbook

November 21, 2020

Instructions & result

We create a guestbook page in which visitors can leave a message. The purpose of the exercise is to practice object-oriented PHP.
  1. Instructions

    We will have a page with a form
    • A field for the username
    • A message field
    • One Button
    (The form will have to be validated and we will not accept pseudonyms of less than 3 characters nor messages of less than 10 characters)
    We will create a "messages" file that will contain one message per line.
    • we will use serialize and an array ["username" => "....", "message" => "....", "date" => ]
    • To serialize the messages we will use the functions json_encode(array) and json_decode(array, true).
  2. Formating

    The page should display all messages under the form formatted as follows:

    Pseudo on 03/12/2009 at 12:00 pm
    The message

    Line breaks must be kept nl2br

  3. Constraints

    1. Use a class to represent a Message
      • new Message(string $username, string $message, DateTime $date = null)
      • isValid(): bool
      • getErrors(): array
      • toHTML(): string
      • toJSON(): string
      • Message::fromJSON(string): Message
    2. Use a class to represent the guestbook
      • new GuestBook($file)
      • addMessage(Message $message)
      • getMessages(): array
Result
Click on one of the images above to access the exercise's website

Code

guestbook.php

Code
<?php
$title = "Guest book";
require_once 'includes/header.php';
require_once 'class/Message.php';
require_once 'class/GuestBook.php';

$errors = null;
$success = false;
$guestbook = new GuestBook(__DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'messages');
if(isset($_POST['username'], $_POST['message'])) {
  $message = new Message($_POST['username'], $_POST['message'], new Datetime());
  if ($message->isValid()) {
    $guestbook->addMessage($message);
    $success = true; // Display a success message is successfully added to the 'messages' file (as a JSON string)
    $_POST = []; // Empty the form;
  } else {
    $errors = $message->getErrors();
  }
}
$messages = $guestbook->getMessages();
?>

<p class="lead mb-5">This guestbook system has been realized with <b>object oriented</b> PHP.</p>

<div class="row">

<div class="col-md-6 mb-4">
  <h2 class="mb-4">Share what you think!</h2>
  <?php if (!empty($errors)): ?>
    <div class="alert alert-danger">Invalid form</div>
  <?php endif; ?>
  <?php if ($success): ?>
    <div class="alert alert-success">Thank you for your message!</div>
  <?php endif; ?>
  <form action="" method="post">
    <div class="form-group">
      <label>Username</label>
      <input type="text" name="username" value="<?= isset($_POST['username']) ? htmlentities($_POST['username']) : '' ?>" class="form-control <?= isset($errors['username']) ? 'is-invalid' : '' ?>"></input>
      <?php if (isset($errors['username'])): ?>
        <div class="invalid-feedback"><?= $errors['username'] ?></div>
      <?php endif; ?>
    </div>
    <div class="form-group">
      <label>Your message</label>
      <textarea name="message" rows="3" class="form-control <?= isset($errors['message']) ? 'is-invalid' : '' ?>"><?= isset($_POST['message']) ? htmlentities($_POST['message']) : '' ?></textarea>      
      <?php if (isset($errors['message'])): ?>
        <div class="invalid-feedback"><?= $errors['message'] ?></div>
      <?php endif; ?>
    </div>
    <button type="submit" class="btn btn-primary">Send a thought</button>
  </form>
  </div>

  <div class="col-md-6">
    <h2 class="mb-4">Posts from our users:</h2>
    <?php if(!empty($messages)): ?>
      <div>
        <?php foreach ($messages as $message): ?>
          <?= $message->toHTML() ?>
        <?php endforeach ?>
      </div>
    <?php else: ?>
      <p>No message yet. Be the first to write one!</p>
    <?php endif; ?>
  </div>

</div>

<?php require 'includes/footer.php'; ?>

GuestBook.php

Code
<?php
require_once 'Message.php';

class GuestBook {

    public $file;

    public function __construct(string $file)
    {
        if (!is_dir(dirname($file))) { // Check if file's path folders exist (if not: create them)
            mkdir(dirname($file), 0777, true);
        }
        if (!file_exists($file)) { // Check if file exists (if not: create it)
            touch($file); // touch() function allows to create a new file.
        }
        $this->file = $file;
    }

    public function addMessage(Message $message): void
    {
       file_put_contents($this->file, $message->toJSON() . PHP_EOL, FILE_APPEND);
    }
    public function getMessages()
    {
        $content = trim(file_get_contents($this->file));
        $messages = [];
        if(!empty($content)) {
            $lines = (array)explode( PHP_EOL, $content);
            foreach ($lines as $line) {
                $messages[] = Message::fromJSON($line);
            }
        }
        return array_reverse($messages);
    }

}

Message.php

Code
<?php
class Message {

    const LIMIT_USERNAME = 3; // We create constants to be able to change them easily if needed later on.
    const LIMIT_MESSAGE = 10;
    private $username;
    private $message;
    private $date;

    public function __construct(string $username, string $message, ?DateTime $date = null) 
        // Question mark before the value type (DateTime) to say: "if value is null, then value will have to be a DateTime object"
    {
        $this->username = $username;
        $this->message = $message;
        $this->date = $date ?: new DateTime(); // If $date = null, thencreate a new DateTime object
    }
    public function isValid(): bool
    {
        return empty($this->getErrors());
    }
    public function getErrors(): array
    {
        $errors = [];
        if (strlen($this->username) < self::LIMIT_USERNAME) {
            $errors['username'] = "Username is too short (3 characters min.)";
        }
        if (strlen($this->message) < self::LIMIT_MESSAGE) {
            $errors['message'] = "Message is too short (10 characters min.)";
        }
        return $errors;
    }
    public function toJSON(): string 
    {
        return json_encode([
            'username' => $this->username,
            'message' => $this->message,
            'date' => $this->date->getTimestamp()
        ]);
    }
    public function toHTML()
    {
        $username = htmlentities($this->username);
        $this->date->setTimeZone(new DateTimeZone('Europe/Paris')); // Change the timezone to Paris on display
        $date = 'on ' . $this->date->format("F n, Y") . ' at ' . $this->date->format("g:i a");
        $message = nl2br(htmlentities($this->message));
            // nl2br() function cahnge the linebreaks that are not readen in HTML into <br> tags
        return <<<HTML
        <div class="card mb-2">
            <div class="card-body">
                <div class="mb-2">
                    <b>$username</b>
                    <span class="text-muted"><i>$date</i></span>
                </div>
                $message
            </div>
        </div>
HTML;
    }
    public static function fromJSON(string $json): Message 
    {
        $data = json_decode($json, true);
        $date = new DateTime("@" . $data['date']);
        return new self($data['username'], $data['message'], $date);
            // It's necessary to convert back into an array of objects because we'll use methods on $message in guestbook.php
            // new DateTime("@$ts") is a shortened version of new DateTime instantiation + setTimestamp() --> ref: https://www.php.net/manual/en/datetime.settimestamp.php ('Notes' paragraph)
    }
}
© 2020 - Edouard Proust | The Developer Fastlane