Php WebLibrary

Download the source archive WebLibrary.zip. Install it as a Php Application with Existing Sources. On MAC or Linux run this command in WebLibrary to ensure web server writability:
$ cd /PATH/TO/PhpLibrary
$ chmod 777 app/cache

MySQL Database

This application uses the same database as Php WebLibraryDB example. In particular, the settings db.php files in the two applications are identical.

If you changed the MySQL database for WebLibraryDB, you must make the changes as well in the include/db.php of WebLibrary.

If necessary, initialize the database by running these from a command shell:

Windows:
> cd \PATH\TO\WebLibrary  
> php setup\makeTables.php
MAC,Linux:
$ cd /PATH/TO/WebLibrary 
$ php setup/makeTables.php

Description

This application supports three access methods: The features of this application are these:
  1. Display listing of books or users. The display supports ordering by field by clicking the column headers.

    By small alterations in the code, the books table content can be presented as a full or paginated list.
  2. A display of book details including which users have borrowed (a copy of) the book.
  3. A display of the user details including which books the user has borrowed.
  4. A login mechanism for users which affords them borrowing and returning privileges. From the book details page, a validated user can
    • borrow a book not already borrowed for which there are copies available.
    • return a borrowed book
  5. The boolean "is_admin" field in the user record marks the user as a administrator.
  6. An admin can add a new book from a menu choice. From the book details, an admin can
    • modify the book fields
    • remove the book if all have been returned
    • return the book from any user

Book and User Listing

The root file, index.php, is programmed to show either the books or the users list. This choice is held in the session memory so that "Home" always maintains what was most recently chosen. As you see, the books choice can be set to correspond to one of 2 possible "view script" alternatives.

index.php
<?php
require_once "include/session.php";
 
// failsafe resets
// unset($session->display);
// unset($session->book_order);
// unset($session->user_order);
 
if (!isset($session->display)) {
  $session->display = 'book';
}
 
if (!isset($session->book_order)) {
  $session->book_order = 'title';
}
 
if (!isset($session->user_order)) {
  $session->user_order = 'name';
}
 
$view_script = [
  // choose one of the books list presentations
  "book" => "books.php", 
  //"book" => "books-page.php", 
 
  "user" => "users.php",
];
 
$target_script = $view_script[$session->display];
require_once($target_script);

setDisplayTable.php
<?php
require_once "include/session.php";
 
$table = filter_input(INPUT_GET, 'table');
 
$session->display = $table;
 
header("location: .");

setBookOrder.php
<?php
require_once "include/session.php";
 
$field = filter_input(INPUT_GET, 'field');
 
$session->book_order = $field;
 
header("location: .");

setUserOrder.php
<?php
require_once "include/session.php";
 
$field = filter_input(INPUT_GET, 'field');
 
$session->user_order = $field;
 
header("location: .");
The menu looks like this:

templates/links.tpl
<li class="dropdown">
  <a href="#" 
     class="dropdown-toggle" 
     data-toggle="dropdown" 
     role="button" 
     aria-haspopup="true" 
     aria-expanded="false">Set Display <span class="caret"></span></a>
  <ul class="dropdown-menu">
    <li><a href="setDisplayTable.php?table=book">Books</a></li>
    <li><a href="setDisplayTable.php?table=user">Users</a></li>
  </ul>
</li>
 
{if $session->login}
  <li><a href="changeUserInfo.php">Change My Info</a></li>
{/if}
 
{if $session->login and $session->login->is_admin}
  <li><a href="addBook.php">Add Book</a></li>
{/if}
 
{if $session->login}
  <li><a href="logout.php">Logout</a></li>
{else}
  <li><a href="login.php">Login</a></li>
{/if}

Standard Listing

The standard listing places fields and values into an HTML table. The column headers act as sorters for that field. The title values are hyperlinks to a page which shows the book information.

books.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
$order = $session->book_order;
 
$books = R::findAll('book', "order by $order");
 
$data = [
    'books' => $books,
];
$smarty->assign($data);
$smarty->display("books.tpl");

templates/books.tpl
{*
books.tpl: display of all books
*}
 
{extends file="layout.tpl"}
 
{block name="content"}
  <h2>Books</h2>
 
  <table class="table table-hover table-condensed">
    <tr>
      <th><a href="setBookOrder.php?field=title">title</a></th>
      <th><a href="setBookOrder.php?field=binding">binding</a></th>
      <th><a href="setBookOrder.php?field=quantity">quantity</a></th>
    </tr>
 
    {foreach $books as $book}
      <tr>
        <td>
          <a href="showBook.php?book_id={$book->id}">{$book->title|escape: 'html'}</a>
        </td>
        <td>{$book->binding}</td>
        <td>{$book->quantity}</td>
      </tr>
    {/foreach}
 
  </table>
{/block}
The users listing is analogous to the books listing.

users.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
$order = $session->user_order;
 
$users = R::findAll('user', "order by $order");
 
$data = [
    'users' => $users,
];
$smarty->assign($data);
$smarty->display("users.tpl");

templates/users.tpl
{*
users.tpl: display of users
*}
 
{extends file="layout.tpl"}
 
{block name="content"}
  <h2>Users</h2>
 
  <table class="table table-hover table-condensed">
    <tr>
      <th><a href="setUserOrder.php?field=name">name</a></th>
      <th><a href="setUserOrder.php?field=email">email</a></th>
    </tr>
    {foreach $users as $user}
      <tr>
        <td><a href="showUser.php?user_id={$user->id}">{$user->name}</a></td>
        <td>{$user->email}</td>
      </tr>
    {/foreach}
  </table>
{/block}

Paginated Book Listing

Database pagination is controlled with the SQL LIMIT clause in a SELECT query:
SELECT ... FROM ... [ WHERE ... ] [ ORDER BY ... ] LIMIT offset,max
The offset value gives the start record (0-based) and the limit value give the maximum number to obtain. If we simply use "LIMIT max" (with one argument), then the offset is assumed to be 0. To get the paginated listing, edit index.php, changing:

index.php
$view_script = [
  ...
  "books" => "books-page.php", 
  ...

books-page.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
$books_per_page = 10;
 
$page = filter_input(INPUT_GET, 'page');
 
if (is_null($page)) {
  $page = 1;
}
$offset = ($page-1) * $books_per_page;
 
$books = R::findAll('book', 
  "order by {$session->book_order} limit $offset,$books_per_page"
);
 
$num_pages = ceil( R::count('book')/$books_per_page );
 
$data = [
    'books' => $books,
    'page' => $page,
    'num_pages' => $num_pages,
];
$smarty->assign($data);
$smarty->display("books-page.tpl");

templates/books-page.tpl
{*
books-page.tpl: display book listed by pages
*}
{extends file="layout.tpl"}
 
{block name="localstyle"}
  <style type="text/css">
    #nav_bar {
      background: #000;
      color: #fff;
      margin-bottom: 15px;
      line-height: 25px;
      height: 25px;
      padding-left: 5px;
      cursor: pointer;
      border-radius: 6px;
    }
    #nav_bar a {
      color: #fff;
      font-weight: bold;
      padding: 0 5px;
      outline: none;
    }
    #nav_bar a:nth-child({$page}) {
      color: magenta;
    }
  </style>
{/block}
 
{block name="content"}
  <h2>Books - paged</h2>
 
  <div id="nav_bar">
    Pages:
    {foreach range(1,$num_pages) as $num}
      <a href="?page={$num}">{$num}</a>
    {/foreach}
  </div>
 
  <table class='table table-condensed table-hover'>
    <tr>
      <th><a href="setBookOrder.php?field=title">title</a></th>
      <th><a href="setBookOrder.php?field=binding">binding</a></th>
      <th><a href="setBookOrder.php?field=quantity">quantity</a></th>
    </tr>
    {foreach $books as $book}
      <tr>
        <td>
          <a href="showBook.php?book_id={$book->id}">{$book->title|escape: 'html'}</a>
        </td>
        <td>{$book->binding}</td>
        <td>{$book->quantity}</td>
      </tr>
    {/foreach}
  </table>
{/block}
The desired page number is passed into the script as the (1-based) page parameter via one of the stylized page hyperlinks in a dedicated nav_bar div element. We use the total number of pages, $num_pages, to present all page hyperlinks. The selected page number is given special recognition by this style rule within the style section:
  #nav_bar a:nth-child({$page}) {
    color: magenta;
  }

User/Book Details

The user display is activated by clicking one of the name hyperlinks in the user listing. The hyperlink holds the user id which then activates the script to look up the user and present both the user field values plus a listing of books borrowed by the user.

Similarly, the book details are activated by clicking the title hyperlink. The portion of the individual book display is completely analogous to the user display shown here. The difference with the book display is that it supports additional functionality which we will see later.

User Details

Here are the scripts used:

showUser.php
<?php
require_once "include/smarty.php";
require_once "include/helper.php";
require_once "include/db.php";
 
$user_id = filter_input(INPUT_GET, 'user_id');
 
$user = R::load('user', $user_id);
 
$userBookRecords = $user->via('borrow')->sharedBook;
 
$data = [
    'user' => $user,
    'userBooks' => $userBookRecords,
    'helper' => new Helper(),
];
$smarty->assign($data);
$smarty->display("showUser.tpl");

templates/showUser.tpl
{*
showUser.tpl: 
show a user and borrowed books
*}
 
{extends file="layout.tpl"}
 
{block name="localstyle"}
  <style type="text/css">
    td:first-child {
      width: 10px;
    }
    td {
      border: none ! important;
    }
  </style>
{/block}
 
{block name="content"}
  <h2>Show User</h2>
 
  <table class="table table-condensed">
    <tr> <td>id:</td> <td>{$user->id}</td> </tr>
    <tr> <td>name:</td> <td>{$user->name}</td> </tr>
    <tr> <td>email:</td> <td>{$user->email}</td> </tr>
  </table>
 
  {if !$user->is_admin}
    <h4>Books Borrowed:</h4>
 
    {foreach $userBooks as $book}
      <a href="showBook.php?book_id={$book->id}">{$book->title}</a>:
      {$helper->getBorrowBookUser($book->id, $user->id)->borrowed_at}
      <br />
    {/foreach }
 
    {* here is an alternative way of printing the books borrowed *}
{*    
    {foreach $user->ownBorrow as $borrow}
      <a href="showBook.php?book_id={$borrow->book->id}">
        {$borrow->book->title}
      </a>:
      {$borrow->borrowed_at}
      <br />
    {/foreach}
*}
  {/if}
 
{/block}

include/helper.php
<?php
require_once "rb.php";
 
class Helper {
  public static function getBorrowBookUser($book_id, $user_id) {
    return R::findOne(
      "borrow", "book_id=? and user_id=?", [$book_id, $user_id]
    );
  }
}

Smarty Helper Functions

For the most part, Php functions operate as Smarty functions. We have created the Helper class and included it into the controller code by:
require_once "include/helper.php";
Inside the controller, we could access the static function:
Helper::getBorrowBookUser(...)
However, attempting to use this approach will fail in Smarty, but the replacement is to create an object of this Helper class, pass that to Smarty, and have Smarty access the static member functions as regular member functions:

showUser.php
...
$data = [
    ...
    'helper' => new Helper(),
];
$smarty->assign($data);
$smarty->display("showUser.tpl");

templates/showUser.tpl
...
{$helper->getBorrowBookUser($book->id, $user->id)->borrowed_at}
...

Alternative listings using "own"

An alternative way of printing the books borrowed by the user is in comments within showUser.tpl. It employs the RedBean "own" feature to go directly to the related borrows. The helper function is not needed.

templates/showUser.tpl
  {foreach $user->ownBorrow as $borrow}
    <a href="showBook.php?book_id={$borrow->book->id}">
      {$borrow->book->title}
    </a>:
    {$borrow->borrowed_at}
    <br />
  {/foreach}
Likewise, we can print the users who have borrowed a given book like this:

templates/showBook.tpl
  {foreach $book->ownBorrow as $borrow}
    <a href="showUser.php?user_id={$borrow->user->id}">
      {$borrow->user->name}
    </a>:
    {$borrow->borrowed_at}
    <br />
  {/foreach}

User Validation

The login/logout user validation uses login.php, validate.php and logout.php in a manner structurally the same name as in Php WebSessions. The difference is that validation is based on both user name and password, taken from the user table records. Our database sets the password to the name for each of the users:
john, kirsten, bill, mary, joan, alice, carla, dave

The $session->login object

The goal of validation is to set $session->login to something when executing the validate.php script. It is useful to make $session->login an object and store in it number of the fields from the user table. The heart of the code is this:
$user = R::findOne("user", "name=?", [$username]);
 
if (!isset($user)) {
  /* no such user */
}
elseif (hash('sha256', $password) === $user->password) { 
  $session->login = (object) [
      'id' => $user->id,
      'name' => $user->name,
      'is_admin' => $user->is_admin,
  ];
  ...
}
else {
  /* failed password entry */
}
So, why not just employ $user from database query:
$user = R::findOne("user", "name=?", [$username]);
...
elseif (hash('sha256', $password) === $user->password) { 
  $session->login = $user
  ...
}
..
The reason is that $user as obtained from the R::findOne operation is a database object which is not serializable, meaning that it cannot be held as a session variable.

The login/logout procedure

The login script submits a user name and password within a form for validate. The session flash variables are used on failed attempts.

login.php
<?php
require_once "include/smarty.php";
 
if (isset($session->login)) {
  header("location: .");
  exit();
}
 
$data = [
];
$smarty->assign( $data );
$smarty->display("login.tpl");

templates/login.tpl
{*
login.tpl: login form
*}
 
{extends file="layout.tpl"}
 
{block name="localstyle"}
  <style>
    th {
      width: 10px;
    }
    th, td {
      border: none ! important;
    }
  </style>
{/block}
 
{block name="content"}
  <h2>Login</h2>
 
  <p>Please enter access information</p>
 
  <form action="validate.php" method="post" autocomplete="off">
    <table class="table table-condensed">
      <tr>
        <th>user:</th>
        <td><input class="form-control" type="text" name="username" autofocus="on"
                   value="{{session_get_flash var='username'}|escape:'html'}" /></td>
      </tr>
      <tr>
        <th>password:</th>
        <td><input class="form-control" type="password" name="password" /></td>
      </tr>
      <tr>
        <td></td>
        <td><button type="submit">Access</button></td>
      </tr>
    </table>
 
  </form>
 
  <h4 id="message">{session_get_flash var='message'}</h4>
{/block}

validate.php
<?php
require_once "include/session.php";
require_once "include/db.php";
 
$username = filter_input(INPUT_POST, 'username');
$password = filter_input(INPUT_POST, 'password');
 
$trim_username = trim($username);  // must be trimmed before using
$user = R::findOne("user", "name=?", [$trim_username]);
 
if (!isset($user)) {
  $session->username = $username;  // flash
  $session->message = "Authentication Failed (username)";  // flash
  header( "location: login.php" );
}
elseif (hash('sha256', $password) === $user->password) { // OK
  $session->login = (object) [
      'id' => $user->id,
      'name' => $user->name,
      'is_admin' => $user->is_admin,
  ];
  header( "location: ." );
}
else {
  $session->username = $username;
  $session->message = "Authentication Failed (password)";
  header( "location: login.php" );
}
Upon failure, we redirect to login.php. For pedagogical reasons we distinguish a failed user lookup from a failed password match. Once logged in the menu system presents new choices:

templates/links.tpl
...
{if $session->login}
  <li><a href="changeUserInfo.php">Change My Info</a></li>
{/if}
 
{if $session->login and $session->login->is_admin}
  <li><a href="addBook.php">Add Book</a></li>
{/if}
 
{if $session->login}
  <li><a href="logout.php">Logout</a></li>
{else}
  <li><a href="login.php">Login</a></li>
{/if}
Logout, as in Php WebSessions, simply means to invalidate $session->user and redirect to the home page.

logout.php
<?php
require_once "include/session.php";
 
unset($session->login);
header( "location: ." );

Featues in showBook for logged-in users

Many of the remaining features of this applications are revealed within the showBook script, only for logged in users. You can see this in the showBook.tpl script:

templates/showBook.tpl
...
{block name="content"}
 
  {if $session->login}  
    <div class='action'>
 
    ...
 
    </div>
  {/if}
 
<h4 id="message">{session_get_flash var='message'}</h4>
{/block}

User: Checkout and Return

Checkout, return and other control features appear within the showBook.php script when the user has logged in. Control settings at the top of the script determine which features should be displayed.

showBook.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
require_once "include/helper.php";
 
$book_id = filter_input(INPUT_GET, 'book_id');
 
$book = R::load('book', $book_id);
 
if ($book->id == 0) {
  // typically would happen when going back after removal
  header("location: .");
  exit();
}
 
$has_book = false;
if ($session->login) {
  $user_id = $session->login->id;
 
  $borrow = Helper::getBorrowBookUser($book_id, $user_id);
  $has_book = !is_null($borrow);  
}
 
// we are going to send "$bookUsers" instead of "$bookUserRecords"
// because it is better suited to usage in the Smarty html_options
 
$bookUserRecords = $book->via('borrow')->sharedUser;
$bookUsers = [];
foreach($bookUserRecords as $record) {
  $bookUsers[$record->id] = $record->name;
}
 
$data = [
    'book' => $book,
    'has_book' => $has_book,    
    'bookUsers' => $bookUsers,
    'helper' => new Helper(),
];
$smarty->assign($data);
$smarty->display("showBook.tpl");

templates/showBook.tpl
  <div class='action'>
    {if not $session->login->is_admin} {* not an admin *}
 
      {if $has_book} 
        <form action="returnBookMyself.php" method="get">
          Do you want to return it ? 
          <input type='hidden' name='book_id' value="{$book->id}" />
          <button type='submit'>Yes</button>
        </form>
 
      {elseif $book->quantity > 0}
        {* does not have book and at least one available *}
        <form action="checkOutBook.php" method="get">
          Do you want to check it out ? 
          <input type='hidden' name='book_id' value="{$book->id}" />
          <button type='submit'>Yes</button>
        </form>
 
      {else}
        <b>No copies available</b>
      {/if}
 
    {else}  {* an admin *}
       ...
    {/if}
  </div>
The two forms have identical structure in that they capture the book_id as a hidden parameter and send it to the target script. Either GET (which we use) or POST methods are OK. POST is probably better, but GET makes things simpler for debugging.

Return script

If the user has the book, a form always appears to allow him/her to return it.

returnBookMyself.php
<?php
require_once "include/session.php";
require_once "include/db.php";
require_once "include/helper.php";
 
if (!isset($session->login)) {  // not authenticated
  header("location: login.php");
  exit();
}
 
$book_id = filter_input(INPUT_GET, 'book_id');
$user_id = $session->login->id;
 
$book = R::load('book', $book_id);
 
//========== Remove brorrow record and increment book quantity
 
unset($book->via('borrow')->sharedUser[$user_id]);
 
// alternatives to unset one-liner
//----------------------
//$borrow = Helper::getBorrowBookUser($book_id, $user_id);
//R::trash($borrow);
//----------------------
//$user = R::load('user', $user_id);
//unset($user->via('borrow')->sharedBook[$book_id]);
//R::store($user);
 
$book->quantity += 1;
R::store($book);
//============================
 
$session->message = "Successfully returned";
 
// to debug, comment out and reload manually
header("location: showBook.php?book_id=$book_id");

Checkout script

Checkout only appears if the user does not have the book and there are some available.

checkOutBook.php
<?php
require_once "include/session.php";
require_once "include/db.php";
 
if (!isset($session->login)) {  // not authenticated
  header("location: login.php");
  exit();
}
 
$book_id = filter_input(INPUT_GET, 'book_id');
 
$book = R::load('book', $book_id);
 
$user = R::load('user', $session->login->id);
 
//=========== add borrow record and decrement book quantity
 
$book->link('borrow', ['borrowed_at' => date("Y-m-d", time())])->user = $user;
 
// alternatives to link one-liner
//---------------------- 
//$user->link('borrow', ['borrowed_at' => date("Y-m-d", time())])->book = $book;
//R::store($user);
//---------------------- 
//$borrow = R::dispense('borrow');
//$borrow->user_id = $user->id;
//$borrow->book_id = $book->id;
//$borrow->borrowed_at = date("Y-m-d", time());
//R::store($borrow);
//
//$borrow = R::dispense('borrow');
//$borrow->user = $user;
//$borrow->book = $book;
//$borrow->borrowed_at = date("Y-m-d", time());
//R::store($borrow);
 
$book->quantity -= 1;
R::store($book);
//=================================
 
$session->message = "Successfully checked out";
 
// to debug, comment out and reload manually
header("location: showBook.php?book_id=$book_id");
We are not dealing with the problem of concurrency, i.e, if two users see the last available copy of the book and decide to borrow it. There are a number of ways to do deal with this issue, but, for simplicity, we are ignoring it.

User: Change Information

Once logged in, a user sees the Change My Info menu choice which allows him/her to change the email. The changeUserInfo.php is directly reentrant in that both the initial and reentrant accesses go through the same top script.

Of significance is the usage of the new Php function
filter_var
which, in this case, tests for validity of the updated email field.

changeUserInfo.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
if (!isset($session->login)) {
  header("location: login.php");
  exit();
}
 
$user = R::load('user', $session->login->id);
 
$email = filter_input(INPUT_POST, 'email');
 
$doit = filter_input(INPUT_POST, 'doit');
$cancel = filter_input(INPUT_POST, 'cancel');
 
if (!is_null($cancel)) {
  header("location: .");
  exit();
}
 
$message = '';
 
if (!is_null($doit)) {
  // reentrant
  try {
    // trim before using
    $trim_email = trim($email);
 
    $is_valid = filter_var($trim_email, FILTER_VALIDATE_EMAIL);
    if ($is_valid === false) {
      throw new Exception("invalid email");
    }
 
    $user->email = $trim_email;
    $id = R::store($user);
    header("location: showUser.php?user_id=$id");
    exit();
  }
  catch (Exception $ex) {
    $message = $ex->getMessage();
  }
}
else {
  // initial
  $email = $user->email;
}
 
$data = [
    'username' => $user->name,
    'email'    => $email,
    'message'  => $message,
];
 
$smarty->assign($data);
$smarty->display("changeUserInfo.tpl");

templates/changeUserInfo.tpl
{*
changeUserInfo.tpl: change information of logged-in user (email only)
*}
 
{extends file="layout.tpl"}
 
{block name="localstyle"}
  <style type="text/css">
    td:first-child {
      width: 10px;
    }
    td {
      border: none ! important;
    }
  </style>
{/block}
 
{block name="content"}
<h2>Change User Info</h2>
  <form action="changeUserInfo.php" method="post">
    <table class="table-condensed table">
      <tr>
        <td>username:</td>
        <td>{$username}</td>
      </tr>
      <tr>
        <td>email:</td>
        <td>
          <input class="form-control" type="text" name="email" 
                 value="{$email|escape:'html'}" />
        </td>
      </tr>
      <tr>
        <td></td>
        <td>
          <button type="submit" name="doit">Change</button>
          <button type="submit" name="cancel">Cancel</button>
        </td>
      </tr>
    </table>
 
    <h4 id='message'>{$message}</h4>
  </form>
{/block}

Admin: Add a Book

An admin is determined by the boolean is_admin field in the user table. In our database, there are two admins: carla and dave. In this application, an admin has different behavior than a non-admin in that he/she doesn't check out and return books.

We can identify an admin user as being logged in and having the is_admin field set to true:
if (isset($session->login) && $session->login->is_admin) { ... }
and the Smarty admin test is:
{if $session->login and $session->login->is_admin}...{/if}
The add book feature appears in the menu only for admins. Our application separates the initial and reentrant activations:
addBook.php: initial
addBookReentrant.php: reentrant
Here are the scripts:

addBook.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
if (!isset($session->login)) {
  header("location: login.php");
  exit();
}
if (!$session->login->is_admin) { // not an admin
  die("Prohibited");
}
 
$bindings = ['paper' => 'paper', 'cloth' => 'cloth'];
 
$data = [
    'bindings' => $bindings,
    // instead of null we could use ""
    'title' => null,
    'quantity' => null,
    'binding' => null,
];
$smarty->assign($data);
$smarty->display("addBook.tpl");

addBookReentrant.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
if (!isset($session->login)) {
  header("location: login.php");
  exit();
}
if (!$session->login->is_admin) { // not an admin
  die("Prohibited");
}
 
//print_r($_POST);
 
$cancel = filter_input(INPUT_POST, 'cancel');
if (!is_null($cancel)) {
  header("location: .");
  exit();
}
 
$title = filter_input(INPUT_POST, 'title');
$quantity = filter_input(INPUT_POST, 'quantity');
$binding = filter_input(INPUT_POST, 'binding'); 
 
$bindings = ['paper' => 'paper', 'cloth' => 'cloth'];
 
try {
  $trim_title = trim($title);
  if (strlen($trim_title) < 3) {
    throw new Exception("title must have at least 3 chars");
  }
 
  $trim_quantity = trim($quantity);
  if (!preg_match("/^\d+$/", $trim_quantity)) {
    throw new Exception("quantity must be a non-negative integer");
  }
 
  $bookWithTitle = R::findOne('book', "title=?", [$trim_title]);
 
  if (!is_null($bookWithTitle)) {
    throw new Exception("book with this title already exists");
  }
 
  $book = R::dispense('book');
 
  $book->title = $trim_title;
  $book->quantity = $trim_quantity;
  $book->binding = $binding;
 
  $id = R::store($book);
  header("location: showBook.php?book_id=$id");
  exit();
}
catch (Exception $ex) {
  $message = $ex->getMessage();
}
 
$data = [
    'title'    => $title,
    'quantity' => $quantity,
    'binding'  => $binding,
    'message'  => $message,
 
    'bindings' => $bindings,
];
$smarty->assign($data);
$smarty->display("addBook.tpl");

templates/addBook.tpl
{*
addBook.tpl: Form for adding a book, 
used by addBook.php and addBookReentrant.php
*}
{extends file="layout.tpl"}
 
{block name="localstyle"}
  <style type="text/css">
    td:first-child {
      width: 10px;
    }
    td {
      border: none ! important;
    }
  </style>
{/block}
 
{block name="content"}
  <h2>Add Book</h2>
 
  <form action="addBookReentrant.php" method="post">
    <table class="table table-condensed">
      <tr>
        <td>title: </td>
        <td>
          <input class="form-control" type="text" name="title" 
                 value="{$title|escape:'html'}" />
        </td>
      </tr>
      <tr>
        <td>binding: </td>
        <td>
          <select class="form-control" name="binding">
            {html_options options=$bindings selected=$binding}
          </select>
        </td>
      </tr>
      <tr>
        <td>quantity: </td>
        <td>
          <input class="form-control" type="text" name="quantity" 
                 value="{$quantity|escape:'html'}" />
        </td>
      </tr>
      <tr>
        <td></td>
        <td>
          <button type="submit" name="doit">Add</button>
          <button type="submit" name="cancel">Cancel</button>
        </td>
      </tr>
    </table>
 
  </form>
 
  <h4 id="message">{$message|default}</h4>
{/block}
Here are some observations:
  1. We distinguish the trimmed field values from those obtained through the form submission, e.g.
    $quantity = filter_input(INPUT_POST, 'quantity');
    ...
    $trim_quantity = trim($quantity);
    
    Doing so allows us to keep the form field exactly as they were if we come back into the form to fix an error.
  2. We use a regular expression to validate the quantity:
    if (!preg_match("/^\d+$/", $trim_quantity)) {
      throw new Exception("illegal quantity format");
    }
    We could also use the filter_var function as we did in changeUserInfo.php; however, the usage is complicated a bit by the need to invalidate negative numbers:
    $flags = [
      'options' => ['min_range' => 0],
    ];
    $is_valid = filter_var($trim_quantity, FILTER_VALIDATE_INT, $flags);
    if ($is_valid === false) {
      throw new Exception("illegal quantity format");
    }

Admin: Modify a Book

The modify and remove features differ from add in that they must start from an existing book. The forms to effect these operations appear in the showBook.php script. Here is the modify form:

templates/showBook.tpl
  <div class='action'>
      {if not $session->login->is_admin} {* not an admin *}
        ...
      {else}  {* an admin *}
        <form action="modifyBook.php" method="get">
          <input type='hidden' name='book_id' value="{$book->id}" />
          <button type="submit">Modify</button>
        </form>
        ...
      {/if}
  </div>
Activating Modify uses the hidden book_id parameter and activates a script to do the modifications:

modifyBook.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
if (!isset($session->login)) {
  header("location: login.php");
  exit();
}
if (!$session->login->is_admin) { // not an admin
  die("Prohibited");
}
 
$book_id = filter_input(INPUT_GET, 'book_id');
$book = R::load('book', $book_id);
 
$bindings = ['paper' => 'paper', 'cloth' => 'cloth'];
 
$data = [
    'title'    => $book->title,
    'quantity' => $book->quantity,
    'binding'  => $book->binding, 
    'book_id'  => $book_id,
 
    'bindings' => $bindings,
];
$smarty->assign($data);
$smarty->display("modifyBook.tpl");

modifyBookReentrant.php
<?php
require_once "include/smarty.php";
require_once "include/db.php";
 
if (!isset($session->login)) {
  header("location: login.php");
  exit();
}
if (!$session->login->is_admin) { // not an admin
  die("Prohibited");
}
 
//print_r($_POST);
 
$book_id = filter_input(INPUT_POST, 'book_id');
 
$cancel = filter_input(INPUT_POST, 'cancel');
 
if (!is_null($cancel)) {
  header("location: showBook.php?book_id=$book_id");
  exit();
}
 
$book = R::load('book', $book_id);
 
$title    = filter_input(INPUT_POST, 'title');
$quantity = filter_input(INPUT_POST, 'quantity');
$binding   = filter_input(INPUT_POST, 'binding');
 
try {
  $trim_title = trim($title);
  if (strlen($trim_title) < 3) {
    throw new Exception("title must have at least 3 chars");
  }
 
  $trim_quantity = trim($quantity);
  if (!preg_match("/^\d+$/", $trim_quantity)) {
    throw new Exception("quantity must be a non-negative integer");
  }
 
  $bookWithTitle = R::findOne('book', "title=?", [$trim_title]);
 
  if (!is_null($bookWithTitle) && $bookWithTitle->id != $book->id) {
    throw new Exception("another book with this title already exists");
  }
 
  $book->title = $trim_title;
  $book->quantity = $trim_quantity;
  $book->binding = $binding;
 
  $id = R::store($book);
  header("location: showBook.php?book_id=$id");
  exit();
}
catch (Exception $ex) {
  $message = $ex->getMessage();
}
 
$bindings = ['paper' => 'paper', 'cloth' => 'cloth'];
 
$data = [
    'message'  => $message,
    'title'    => $title,
    'quantity' => $quantity,
    'binding'  => $binding,
 
    'book_id'  => $book_id,
 
    'bindings' => $bindings,
];
$smarty->assign($data);
$smarty->display("modifyBook.tpl");
This modifyBook.php script is very much like that in addBook.php. The differences are these:
  1. You must maintain the book_id value as the way of accessing the book on each activation. You cannot "hold it in the session"!

    modifyBook*.php
    $book_id = /* passed in from calling script */;
    $book = R::load('book', $book_id);
    The $book_id is sent initially to modifyBook.php as a GET parameter and so is captured like this. It could be POST, but having it as GET makes debugging easier.

    modifyBook.php
    $book_id = filter_input(INPUT_GET, 'book_id');
    The subsequent calls to modifyBookReentrant.php are of the POST method from the form — this is more-or-less a requirement — and are captured as:

    modifyBookReentrant.php
    $book_id = filter_input(INPUT_POST, 'book_id');
    This difference illustrates a potential advantage of separating the initial and reentrant activations into different scripts. The filter_input function provides no way to do "either from get or from post."
  2. The statement from addBook.php which creates a fresh book is not present in modifyBook.php:
    $book = R::dispense('book');
  3. The initial script, modifyBook.php, has code which set the initial fields from the existing record. The initial binding value sets the correct initial selection of paper/cloth.

    modifyBook.php
    $data = [
        'title'    => $book->title,
        'quantity' => $book->quantity,
        'binding'  => $book->binding, 
        'book_id'  => $book_id,
        ...
    ];
Within the template script, the only real difference from the add template is the presence of the necessary hidden input field within the form:

modifyBook.tpl
...
<form action="modifyBookReentrant.php" method="post">
 
   <input type="hidden" name="book_id" value="{$book_id}" />
...

Admin: Remove Book

Here is the remove form:

templates/showBook.tpl
  <div class='action'>
      {if not $session->login->is_admin} {* not an admin *}
        ...
      {else}  {* an admin *}
        ...
        <form action="removeBook.php" method="get">
          <button type="submit">
              {{session_get_flash var='title'}|default:'Remove'}
          </button>
          <input type='hidden' name='book_id' value="{$book->id}" />
          <input type='hidden' name='confirm' 
                 value='{session_get_flash var='confirm'}' />
        </form>
        ...
      {/if}
  </div>
Activating Remove sends the book_id and a confirm value to:

removeBook.php
<?php
require_once "include/session.php";
require_once "include/db.php";
 
if (!isset($session->login)) {  // not authenticated
  header("location: login.php");
  exit();
}
if (!$session->login->is_admin) { // not an admin
  die("Prohibited");
}
 
$book_id = filter_input(INPUT_GET, 'book_id');
$confirm = filter_input(INPUT_GET, 'confirm');
 
$book = R::load('book', $book_id);
 
$borrowers = $book->via('borrow')->sharedUser;
 
if (count($borrowers) > 0) {
  $session->message = "All copies must be returned.";
  header("location: showBook.php?book_id=$book_id");
  exit();
}
 
if (!$confirm) {
  // go back with message, button title change, and confirm setting
  $session->message = "Press again to confirm removal";
  $session->button_title = "Confirm Remove";
  $session->confirm = 1;
  header("location: showBook.php?book_id=$book_id");
  exit();
}
 
R::trash($book);
header("location: .");
On first activation, the session flash variable confirm is empty, which behaves as false. We go back in, but set the confirm parameter to 1, change the button title, and give the user an instructional message.

Admin: Return Book from User

The last feature is the ability of an admin to return books from any user. In showBook.php, the superuser is presented with a list of all users of the book (as generated in the book details).

templates/showBook.tpl
  <div class='action'>
      {if not $session->login->is_admin} {* not an admin *}
        ...
      {else}  {* an admin *}
        ...
        {if $bookUsers}
          <form action="returnBookFromUser.php" method="post">
            <input type='hidden' name='book_id' value="{$book->id}" />
            <button type="submit">Return from</button> <b>:</b>
            <select name="user_id">
              {foreach $bookUsers as $user}
                <option value="{$user->id}">{$user->name}</option>
              {/foreach}   
            </select>
          </form>
        {/if}
      {/if}
  </div>
In this case, selection list generated from users which have the books is created more simply by generating the options in a loop than trying to employ the Smarty html_options function. The reason is that html_options expects to use an array map, and the $bookUsers array is of database objects. The Return from button sends both the book_id and user_id to the target script:

returnBookFromUser.php
<?php
require_once "include/session.php";
require_once "include/db.php";
require_once "include/helper.php";
 
if (!isset($session->login)) {  // not authenticated
  header("location: login.php");
  exit();
}
if (!$session->login->is_admin) { // not an admin
  die("Prohibited");
}
 
$book_id = filter_input(INPUT_GET, 'book_id');
$user_id = filter_input(INPUT_GET, 'user_id');
 
$borrow = Helper::getBorrowBookUser($book_id, $user_id);
R::trash($borrow);
 
$book = R::load('book',$book_id);
$book->quantity += 1;
R::store($book);
 
$user = R::load('user',$user_id);
$session->message = "Copy returned from $user->name";
 
header("location: showBook.php?book_id=$book_id");


© Robert M. Kline