Fuel Library

This document and demo application is intended serve the role of Php WebLibrary The same MySQL database is assumed and same tables used.

Installation

Make sure you have the required plugins installed in NetBeans: PHP, Smarty, FuelPHP. Also make sure that Apache rewrite is enabled (it is by default in XAMPP).

Download the source archive FuelLibrary.zip. Extract the archive into the folder which holds your "web projects," which we will assume corresponds to the "/default" URL. Windows users are advised to download the archive and extract it using 7-zip.

Understand exactly what is the "php" command shell command on your system.
  1. Create FuelLibrary as a NetBeans Php Project from Existing Sources. On the run configuration step, make the Project URL be:
    http://localhost/default/FuelLibrary/public
    
  2. On MAC/Linux, open a command shell, navigate to the FuelLibrary directory and run:
    $ php oil r install
    
  3. Double-check the RewriteBase in public/.htaccess. It is currently:
    RewriteBase  /default/FuelLibrary/public
    It is meant to work as is if you use the "/default" URL as suggested.
  4. Enable the FuelPhp features for the project:
    • Right-click on the FuelLibrary project in the Projects window and select Properties.
    • Locate FuelPhp in Frameworks check the enabled checkbox.
  5. Enable FuelPHP code completion in NetBeans. Right-click on the FuelLibrary project and select from the menu:
    FuelPHP ⇾ generate auto-completion file

Specify settings in the database config file

Double-check the settings in the FuelPhp database config file:

fuel/app/config/development/db.php
<?php
/**
 * The development database settings. These get merged with the global settings.
 */
 
$which = 'mysql'; // either 'mysql' or 'sqlite'
 
$mysql = [
  // change these MySQL database specs if necessary
  'connection'  => [
    'dsn'        => 'mysql:host=127.0.0.1;dbname=test',
    //'dsn'        => 'mysql:host=localhost;dbname=test',
    'username'   => 'guest',
    'password'   => '',
  ],
];
 
$sqlite = [
  'charset' => NULL,
  'connection'  => [
    'dsn' => 'sqlite:' . APPPATH . 'database/db.sqlite',
  ],
];
 
$choices = [
  'mysql' => $mysql,
  'sqlite' => $sqlite,
];
 
return [
  'which' => $which,
  'default' => $choices[$which],  // makes this the one used
];

Database Initialization

This application, like all the others assumes the usage of the MySQL test database accessible by the guest user with empty password. The tables and data used are identical to that in Php WebLibrary, so you are most likely good to go. To initialize the database in FuelPhp, run this task:
$ php oil r maketables
A Php task is a class which resides in the directory
fuel/app/tasks
Specifically, your are executing the static run function in the class MakeTables which is found in the file
fuel/app/tasks/maketables.php
The run function is broken down into two run functions found in classes contained in the files
createtables.php
populatetables.php
The latter one, populatetables.php offers some very useful examples about how to work with the FuelPHP ORM.
fuel/app/tasks/populatetables.php  

Model Classes

The ORM Model classes are exactly the same as those used in Fuel LibraryDB

The Base Controller

All controllers used in this application are derived from the base controller:

fuel/app/classes/controller/base.php
<?php
 
class Controller_Base extends Controller {
 
  public function after($response) {
    $response = parent::after($response);
    $response->set_header(
        'Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate'
    );
    return $response;
  }
 
}
The purpose of the after function in the base controller is to attempt to force non-caching of all pages in this application.

Book/User Listing

The home controller manages the listings. The default root action, /home/index, shows either books or users list. It also manages the ordering of the books and users. Here is the home controller code:
fuel/app/classes/controller/home.php  
The display choice is managed through the session and initialized in the before function:
class Controller_Home extends Controller_Base {
 
  public function before() {
    parent::before();
    if (is_null(Session::get('display'))) {
      Session::set('display','book');
    }
    if (is_null(Session::get('book_order'))) {
      Session::set('book_order','title');
    }
    if (is_null(Session::get('user_order'))) {
      Session::set('user_order','name');
    }
  }
  ...
The initialization default makes the book table be displayed with records ordered by title. After before completes, the action_index function is called on home page activation:
  public function action_index() {
    switch (Session::get('display')) {
      case 'book':
        // pick one of these two:
        return $this->bookDisplay();
        //return $this->bookPageDisplay();
 
      case 'user':
        return $this->userDisplay();
    }
  }
We would first call this:
  private function bookDisplay() {
    $books = Model_Book::find('all', [
        'order_by' => [ Session::get('book_order') ],
    ]);
    $data = [
        'books' => $books,
    ];
    return View::forge('home/books.tpl', $data);
  }

Changing Display

The menu portion which resets the display is:

fuel/app/views/links.tpl
  <ul class="dropdown-menu">
    <li>{html_anchor href='/home/setDisplay/book' text='Books'}</li>
    <li>{html_anchor href='/home/setDisplay/user' text='Users'}</li>
  </ul>
This is the first we've seen of a URL with an additional argument:
/ctrl/act/arg
The third argument is passed to the action function as a parameter:
class Controller_Ctrl ... {
  public function action_act($param) {
    // $param = arg
  }
}
Changing display calls the action function:
  public function action_setDisplay($table) {
    //return "setDisplay: $table";
 
    Session::set('display',$table);
    return Response::redirect("/");
  }
As you see, the display session variable is set to the incoming argument (third segment of the URL) and we redirect back to the home page.

Standard Book Listing

As you can see in the code, the intention is that there are two possible listing styles based on one of these choices:
return $this->bookDisplay();
//return $this->bookPageDisplay();
The standard listing has the target view file is:

fuel/app/views/home/books.tpl
{* books.tpl: display of all books *}
 
{extends file="layout.tpl"}
 
{block name="localstyle"}
{/block}
 
{block name="content"}
  <h2>Books</h2>
 
  <table class="table table-hover table-condensed">
    <tr>
      <th>{html_anchor href='/home/setBookOrder/title' text='title'}</th>
      <th>{html_anchor href='/home/setBookOrder/binding' text='binding'}</th>
      <th>{html_anchor href='/home/setBookOrder/quantity' text='quantity'}</th>
    </tr>
    {foreach $books as $book}
      <tr>
        <td>
          {html_anchor href="/show/book/{$book->id}" text="{$book->title}"}
        </td>
        <td>{$book->binding}</td>
        <td>{$book->quantity}</td>
      </tr>
    {/foreach}
  </table>
{/block}

Paginated Book Listing

The paginated listing has a different controller part and its view script adds on a navigation bar below the header:
class Controller_Home extends Controller_Base {
 
  private function bookPageDisplay() {
    $perpage = 8;
 
    $page = Input::param('page');
    if (is_null($page)) {
      $page = 1;
    }
    $books = Model_Book::find('all', [
        'order_by' => [ Session::get('book_order') ],
        'offset' => ($page-1) * $perpage,
        'limit' => $perpage,
    ]);
    $num_pages = ceil(Model_Book::count()/$perpage);
    $data = [
        'books' => $books,
        'num_pages' => $num_pages,
        'page' => $page,
    ];
    return View::forge('home/booksPage.tpl', $data);
  }

fuel/app/views/home/books-page.tpl
...
{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>
...
{/block}
Here we can use a regular hyperlink because we don't need the URL base prepended to get the desired result.

User Listing

This follows the same scheme as Standard Book Listing.

Field Ordering Change

Change of field ordering is effected through the table header hyperlinks in the book listing:

fuel/app/views/home/books.tpl
<tr>
  <th>{html_anchor href='/home/setBookOrder/title' text='title'}</th>
  <th>{html_anchor href='/home/setBookOrder/binding' text='binding'}</th>
  <th>{html_anchor href='/home/setBookOrder/quantity' text='quantity'}</th>
</tr>
and in the user listing:

fuel/app/views/home/users.tpl
<tr>
  <th>{html_anchor href='/home/setUserOrder/name' text='name'}</th>
  <th>{html_anchor href='/home/setUserOrder/email' text='email'}</th>
</tr>
The actions function which control it are
class Controller_Home extends Controller_Base {
 
  public function action_setBookOrder($field) {
    //return "setBookOrder: $field";
 
    Session::set('book_order', $field);
    return Response::redirect("/");
  }
 
  public function action_setUserOrder($field) {
    //return "setUserOrder: $field";
 
    Session::set('user_order', $field);
    return Response::redirect("/");
  }

User/Book Details

The controller for these features is the show controller:
fuel/app/classes/controller/show.php  

User Details

The user display is activated by clicking the name hyperlinks in the user listing like this:

fuel/app/views/home/users.tpl
{html_anchor href="/show/user/{$user->id}" text="{$user->name}"}
The user id is given as a parameter to the target action function. The portion relative to showing a user is this:
class Controller_Show extends Controller_Base {
 
  public function action_user($user_id) {
    //return "user: $user_id";
 
    $data = [
        'user' => Model_User::find($user_id),
    ];
    $view = View::forge("home/showUser.tpl", $data);
    $view->set_safe( 'helper', new Helper() );
    return $view;
  }
The functioning of the date display is due to the helper class:

fuel/app/classes/helper.php
<?php
 
class Helper {
  public static function getBorrowBookUser($book_id, $user_id) {
    return Model_Borrow::find('first', [
      'where' => ['book_id' => $book_id, 'user_id' => $user_id,]
    ]);
  }
}
An instance of the Helper class is passed to the view script, but it must be passed without being sanitized by FuelPhp, much for the same reason as the $validator object that we've seen before. The view script is

fuel/app/views/home/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>
    <tr>
  </table>
 
  {if !$user->is_admin}
    <h4>Books Borrowed:</h4>
    {foreach $user->borrowed_books as $book}
      {html_anchor href="/show/book/{$book->id}" text="{$book->title}"}
      (on {$helper->getBorrowBookUser($book->id, $user->id)->borrowed_at})
      <br />
    {/foreach }
 
    <h4 id="message">{session_get_flash var='message'}</h4>
  {/if}
{/block}

Book Details

The book display is analogous, but we send down different information. We send down the actual borrow records. We'll study this further later, but to see it for now:
class Controller_Show extends Controller_Base {
 
  public function action_book($book_id) {
    //return "book: $book_id";
 
    $book = Model_Book::find($book_id);
 
    if (is_null(!$book)) {
      // typically would happen when going back after removal
      return Response::redirect("/");
    }
 
    $login = Session::get('login');
 
    $has_book = false;
    if (!is_null($login)) {
      $has_book = Helper::getBorrowBookUser($book_id, $login->id) !== null;
    }
 
    $borrowers = [];
    foreach($book->borrowers as $user) {
      $borrowers[$user->id] = $user->name;
    }
 
    $data = [
        'book' => $book,
        'has_book' => $has_book,
        'borrowers' => $borrowers,
    ];
    $view = View::forge("home/showBook.tpl", $data);
    return $view;
  }
The Smarty template script (top part) is:

fuel/app/views/home/showBook.tpl
{block name="content"}
  <h2>Show Book</h2>
 
  <table class="table table-condensed">
    <tr> <td>id:</td> <td>{$book->id}</td> </tr>
    <tr> <td>title:</td> <td>{$book->title|escape:'html'}</td> </tr>
    <tr> <td>binding:</td> <td>{$book->binding}</td> </tr>
    <tr> <td>quantity:</td> <td>{$book->quantity}</td> </tr>
  </table>
 
  <h4>Borrowers:</h4>
  <div>
    {if $book->borrows}
      {foreach $book->borrows as $borrow}
        {html_anchor href="/show/user/{$borrow->user->id}" 
                     text="{$borrow->user->name}"}
        (on {$borrow->borrowed_at})
        <br />
      {/foreach}
    {else}
      None.
    {/if}
  </div>
  ...
{/block}

Authentication

The login/validate/logout functions are within the dedicated authenticate controller.
fuel/app/classes/authenticate/authenticate.php  
The menu links accesses them by:

fuel/app/views/links.tpl
{if $session->get('user')}
  <li>{html_anchor href='/authenticate/logout' text='Logout'}</li>
{else}
  <li>{html_anchor href='/authenticate/login' text='Login'}</li>
{/if}
The login starts as this function:
class Controller_Authenticate extends Controller_Base {
 
  public function action_login() {
    if (!is_null(Session::get('login'))) {
      return Response::redirect("/");
    }
    /* We want to pass username flash variable from the controller, 
     * rather than retrieve it in login.tpl so that it is sanitized.
     */
    $view = View::forge("authenticate/login.tpl");
    $view->set('username', Session::get_flash('username'));
    return $view;
  }
The login view script is:

fuel/app/views/authenticate/login.tpl
{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>Login</h2>
 
  <p>Please enter access information</p>
  {form attrs=['action' => '/authenticate/validate']}
  <table class="table table-condensed">
    <tr>
      <td>user:</td>
      <td><input class="form-control" type="text" name="username" autofocus="on"
                 value="{$username}" /></td>
    </tr>
    <tr>
      <td>password:</td>
      <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}
Initially, and if validation fails, we reenter the login action, setting the username and a failure message through flash variables. The message can be read directly in the login script; however, the username must be escaped, and so it is read and passed down from the controller.

The validation takes place in this method:
class Controller_Authenticate extends Controller_Base {
 
  public function action_validate() {
    $username = Input::post('username');
    $password = Input::post('password');
    $trim_username = trim($username);
 
    $user = Model_User::find('first', [
        'where'=> [ "name" => $trim_username ],
    ]);
    if (is_null($user)) {
      Session::set_flash('username', $trim_username);
      Session::set_flash('message', 'invalid user');
      return Response::redirect('/authenticate/login');
    }
    elseif (hash('sha256', $password) === $user->password) {
      $login = (object) [
         'id' => $user->id,
         'name' => $user->name,
         'is_admin' => $user->is_admin,
      ];
      Session::set('login', $login);
      return Response::redirect('/');      
    }
    else {
      Session::set_flash('username', $username);
      Session::set_flash('message', 'invalid password');
      return Response::redirect('/authenticate/login');
    }
  }
  ...
Although it seems that we should be able to simply set the session user to the database user, we cannot do so because the database user cannot be serialized for session storage.

Checkout and Return

These features are available only to an authenticated user. We group them under the user controller class:
fuel/app/classes/controller/user.php  
It is at this point that we start restricting access by virtue of the validation settings generated in the base controller. Checkout, return and other control features appear as actions within the user controller, which is protected against unauthorized access by the before function which redirects the user to the login page if not validated:
class Controller_User extends Controller_Base {
 
  public function before() {
    parent::before();
    $login = Session::get('login');
    if (is_null($login)) {
      return Response::redirect('/authenticate/login');
    }
    $this->user = Model_User::find($login->id);
  }
  ...
Settings computed within the controller determine which features should be displayed for the home/showBook action:
class Controller_Show extends Controller_Base {
 
  public function action_book($book_id) {
    //return "book: $book_id";
 
    $book = Model_Book::find($book_id);
 
    if (is_null(!$book)) {
      // typically would happen when going back after removal
      return Response::redirect("/");
    }
 
    $login = Session::get('login');
 
    $has_book = false;
    if (!is_null($login)) {
      $has_book = Helper::getBorrowBookUser($book_id, $login->id) !== null;
    }
 
    $borrowers = [];
    foreach($book->borrowers as $user) {
      $borrowers[$user->id] = $user->name;
    }
 
    $data = [
        'book' => $book,
        'has_book' => $has_book,
        'borrowers' => $borrowers,
    ];
    $view = View::forge("home/showBook.tpl", $data);
    return $view;
  }
  ...
For reference, we're again using the static function defined in the class:
fuel/app/classes/helper.php  
The relevant view script portion is this:

fuel/app/views/home/showBook.tpl
  {if $session->get('login')}
    <div class='action'>
 
      {if not $session->get('login')->is_admin} 
        {* not an admin *}
 
        {if $has_book}
 
          {form attrs=['method'=>'get',
             'action' => "/user/returnBookMyself/{$book->id}"]}
          Do you want to return it ? 
          <button type='submit'>Yes</button>
          {/form}
 
        {elseif $book->quantity > 0}
          {* does not have book and at least one available *}
          {form attrs=['method'=>'get',
                 'action' => "/user/checkOutBook/{$book->id}"]}
          Do you want to check it out ? 
          <button type='submit'>Yes</button>
          {/form}
 
        {else}
          <b>No copies available</b>
        {/if}
 
      {else}  
        ...
The checkout and return handlers are these:
class Controller_User extends Controller_Base {
  ...
  public function action_checkOutBook($book_id) {
    //return "checkOutBook: $book_id";
 
    $book = Model_Book::find($book_id);
 
    $borrow = Model_Borrow::forge();
    $borrow->user = $this->user;
    $borrow->book = $book;
    $borrow->borrowed_at = date("Y-m-d", time());
    $borrow->save();
 
    $book->quantity -= 1;
    $book->save();
 
    Session::set_flash("message","checked out");
    return Response::redirect("/show/book/$book_id");
  }
 
  public function action_returnBookMyself($book_id) {
    //return "returnBookMyself: $book_id";
 
    $book = Model_Book::find($book_id);
    unset($book->borrowers[$this->user->id]);
    $book->quantity += 1;
    $book->save();
    //return "success";
    Session::set_flash("message","returned");
    return Response::redirect("/show/book/$book_id");
  }

Change User Information

Here is the action function in the user controller:
class Controller_User extends Controller_Base {
 
  public function action_changeInfo() {
    //return "changeInfo";
 
    $user = Model_User::find($this->sessionUser->id);
 
    // build validator directly right here
    $validator = Validation::forge();
    $validator->add('email')
        ->add_rule('trim')
        ->add_rule('required')
        ->add_rule('valid_email')
    ;
 
    $doit = Input::post('doit');
    $message = '';
    if (!is_null($doit)) {
      try {
        $validated = $validator->run(Input::post());
        if (!$validated) {
          throw new Exception();
        }
        $validData = $validator->validated();
        $user->email = $validData['email'];
        $user->save();
        Session::set_flash('message', "successful update");
        Response::redirect( "/show/user/$user->id" );
      } 
      catch (Exception $ex) {
        $message = $ex->getMessage();
      }
    }
 
    $data = array(
        'email' => $user->email,
        'username' => $user->name,
        'message' => $message,
    );
    $view = View::forge("user/changeInfo.tpl", $data);
    $view->set('validator', $validator, false); // pass validator
    return $view;
  }
The corresponding view script is:

fuel/app/views/user/changeInfo.tpl
{extends file="layout.tpl"}
 
{block name='localstyle'}
  <style type='text/css'>
    td:first-child {
      width: 10px;
    }
    td {
      border: none ! important;
    }
    .error { color: red; font-size: 80%; font-weight:bold; }
  </style>
{/block}
 
{block name="content"}
  <h2>Change User Info</h2>  
  {form}
  <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}" />
        <span class="error">{$validator->error('email')}</span>
      </td>
    </tr>
    <tr>
      <td></td>
      <td>
        <button type="submit" name="doit">Change</button>
        <button type="submit" name="cancel">Cancel</button>
      </td>
    </tr>
  </table>
  {/form}
 
  <h4 id='message'>{$message}</h4>
 
  {* put this in to debug problems with error handling *}
  {*
  <pre>{$validator->error_message()|var_export}</pre>
  *}
 
{/block}

Book Add/Modify

Book addition and modification is meant for an admin user. We create a dedicated admin controller to handle it:
fuel/app/classes/controller/admin.php  
This code is used to protect access to the admin controller:
class Controller_Admin extends Controller_Base {
 
  public function before() {
    parent::before();
    if (is_null(Session::get('login'))) {
      return Response::redirect('/authenticate/login');
    }
    if (!Session::get('login')->is_admin) {
      return Response::redirect('/authenticate/noAccess');
    }
  }
  ...
For a user who as not logged in (like a session timeout) the code redirects to login. We also want to protect access from a user who is logged in, but is not an admin; however, in this case we want to be more blunt in our response.

The validators

In order to reduce the admin controller code, the add and modify validators have been moved into a dedicated class
fuel/app/classes/validators.php
In here, they are accessed as static functions.

fuel/app/classes/validators.php
<?php
 
class Validators {
 
  public static function bookValidator() {
    $validator = Validation::forge();
 
    $validator->add('title', 'title')
        ->add_rule('trim')
        ->add_rule('required')
        ->add_rule('min_length', 3)
    ;
    $validator->add('quantity', 'quantity')
        ->add_rule('trim')
        ->add_rule('required')
        ->add_rule('match_pattern', '/^\d+$/')
    ;
    // enter all fields into validator, regardless of whether
    // they can generate errors or not
    $validator->add('binding', 'binding');
 
    // modify error messages
    $validator
        ->set_message('required', ':label cannot be empty')
        ->set_message('min_length', 'at least :param:1 char(s)')
    ;
    $validator
        ->field('quantity')
        ->set_error_message('match_pattern', 'must be non-neg. integer')
    ;
    return $validator;
  }
 
  public static function addBookValidator() {
    $validator = self::bookValidator();
 
    $isUnique = function($title) {
      $bookWithTitle = Model_Book::find('first', [
              'where' => [
                  [ 'title', $title ]
              ]
      ]);
      return is_null($bookWithTitle);      
    };
 
    // get the title field already added
    $validator->field('title')
        ->add_rule(['unique' => $isUnique])
    ;
 
    $validator
        ->set_message('unique', 'a duplicate exists')
    ;
 
    return $validator;
  }
 
  public static  function modifyBookValidator($book_id) {
    $validator = self::bookValidator();
 
    $isUnique = function($title, $book_id) {
      $bookWithTitle = Model_Book::find('first', [
              'where' => [
                  [ 'title', $title ]
              ]
      ]);
      return is_null($bookWithTitle) || $bookWithTitle->id == $book_id;
    };
 
    // get the title field already added
    $validator->field('title')
        ->add_rule(['unique' => $isUnique], $book_id)
    ;
 
    $validator
        ->set_message('unique', 'a duplicate exists')
    ;
 
    return $validator;
  }
}
Observe that both the addBookValidator and modifyBookValidator are based on the bookValidator which does all the validation except the title uniqueness detection.

Add

The addBook script becomes available through the menu system at this point:

fuel/app/views/links.tpl
{if $session->get('login') and $session->get('login')->is_admin}
  <li>{html_anchor href='/admin/addBook' text='Add Book'}</li>
{/if}
Here is the template used:

fuel/app/views/admin/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;
    }
    .error { color: red; font-size: 80%; font-weight:bold; }
  </style>
{/block}
 
{block name="content"}
  <h2>Add Book</h2>
 
  {form attrs=['action' => "/admin/addBookReentrant"]}
  <table class='table table-condensed'>
    <tr>
      <td>title: </td>
      <td>
        <input class="form-control" type="text" name="title" 
               value="{$title|default}"  />
        <span class="error">{$validator->error_message('title')}</span>
      </td>
    </tr>
    <tr>
      <td>binding: </td>
      <td><select class="form-control" name="binding">
          {html_options options=$bindings  selected={$binding|default}}
        </select>
      </td>
    </tr>
    <tr>
      <td>quantity: </td>
      <td>
        <input class="form-control" type="text" name="quantity" 
               value="{$quantity|default}"  />
        <span class="error">{$validator->error_message('quantity')}</span>
      </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>
 
  {*
  <pre>{$validator->error_message()|var_export}</pre>
  *}
 
{/block}
The addBook/addBookReentrant actions are:
  public function action_addBook() {
    //return "addBook";
    //
    // When initial and reentrant controllers are separate, no validation is
    // done on initial entry; so we just need a "placeholder" for validator.
    $validator = Validation::forge();
 
    $data = [
        'bindings' => $this->getBookBindings(),
        'reentrantUrl' => "admin/addBookReentrant",
        'page_title' => 'Add Book',
    ];
 
    $view = View::forge("admin/addBook.tpl", $data);
    $view->set_safe('validator', $validator); // pass validator
    return $view;
  }
 
  public function action_addBookReentrant() {
    //return "addBookReentrant";
 
    $cancel = Input::post('cancel');
    if (!is_null($cancel)) {
      return Response::redirect("/");
    }
 
    //$validator = Validators::bookValidator();
    $validator = Validators::addBookValidator();
 
    $message = "";
    try {
      $validated = $validator->run(Input::post());
      if (!$validated) {
        throw new Exception();
      }
      $validData = $validator->validated();
 
      $book = Model_Book::forge();
      $book->title = $validData['title'];
      $book->binding = $validData['binding'];
      $book->quantity = $validData['quantity'];
      $book->save();
 
      return Response::redirect("/show/book/$book->id");
    }
    catch (Exception $ex) {
      $message = $ex->getMessage();
    }
 
    // If we get here, we have an error of some kind.
    $data = [
        'bindings' => $this->getBookBindings(),
        'title' => Input::post('title'),
        'binding' => Input::post('binding'),
        'quantity' => Input::post('quantity'),
        'message' => $message,
    ];
 
    $view = View::forge("admin/addBook.tpl", $data);
    $view->set_safe('validator', $validator); // pass validator
    return $view;
  }

Modify

Modify uses the same basic structure with following changes. It is, of course, invoked in a different way than add, through the showBook view:

fuel/app/views/home/showBook.tpl
{form attrs=['action' => "/admin/modifyBook/{$book->id}", 
      'method'=>'get']}
  <button type="submit">Modify</button>
{/form}
The view script is

fuel/app/views/admin/modifyBook.tpl
{*
modifyBook.tpl: Form for modifying a book, 
used by modifyBook.php and modifyBookReentrant.php
*}
 
{extends file="layout.tpl"}
 
{block name='localstyle'}
  <style type='text/css'>
    td:first-child {
      width: 10px;
    }
    td {
      border: none ! important;
    }
    .error { color: red; font-size: 80%; font-weight:bold; }
  </style>
{/block}
 
{block name="content"}
  <h2>Modify Book</h2>
 
  {form attrs=['action' => "/admin/modifyBookReentrant/{$book_id}"]}
  <table class='table table-condensed'>
    <tr>
      <td>title: </td>
      <td>
        <input class="form-control" type="text" name="title" value="{$title}" />
        <span class="error">{$validator->error_message('title')}</span>
      </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}"  />
        <span class="error">{$validator->error_message('quantity')}</span>
      </td>
    </tr>
    <tr>
      <td></td>
      <td>
        <button type="submit" name="doit">Modify</button>
        <button type="submit" name="cancel">Cancel</button>
      </td>
    </tr>
  </table>
  {/form}
 
  <h4 id="message">{$message|default}</h4>
 
  {*
  <pre>{$validator->error_message()|var_export}</pre>
  *}
 
{/block}
The controller code for modify is much like that for add:
  public function action_modifyBook($book_id) {
    //return "modifyBook: $book_id";
 
    $book = Model_Book::find($book_id);
 
    $validator = Validation::forge();
    $data = [
        'bindings' => $this->getBookBindings(),
        'title' => $book->title,
        'book_id' => $book_id,
        'binding' => $book->binding,
        'quantity' => $book->quantity,
    ];
    $view = View::forge("admin/modifyBook.tpl", $data);
    $view->set_safe('validator', $validator); // pass validator
    return $view;
  }
 
  public function action_modifyBookReentrant($book_id) {
    //return "modifyBookReentrant; $book_id";
 
    $cancel = Input::post('cancel');
    if (!is_null($cancel)) {
      return Response::redirect("/show/book/$book_id");
    }
    $book = Model_Book::find($book_id);
 
    //$validator = Validators::bookValidator();
    $validator = Validators::modifyBookValidator($book_id);    
 
    try {
      $validated = $validator->run(Input::post());
      if (!$validated) {
        throw new Exception();
      }
      $validData = $validator->validated();
 
      // OK to go ahead and modify
      $book->title = $validData['title'];
      $book->binding = $validData['binding'];
      $book->quantity = $validData['quantity'];
      $book->save();
 
      return Response::redirect("/show/book/$book->id");
    }
    catch (Exception $ex) {
      $message = $ex->getMessage();
    }
 
    // If we get here, we have an error.
    $data = [
        'bindings' => $this->getBookBindings(),
        'title' => Input::post('title'),
        'binding' => Input::post('binding'),
        'quantity' => Input::post('quantity'),
        'book_id' => $book_id,
        'message' => $message,
    ];
 
    $view = View::forge("admin/modifyBook.tpl", $data);
    $view->set_safe('validator', $validator); // pass validator
    return $view;
  }

Remove

Like modify, remove comes from the showBook script:

fuel/app/views/home/showBook.tpl
{form attrs=['action' => "/admin/removeBook/{$book->id}", 
     'method'=>'get']}
  <button type="submit">
    {{session_get_flash var='button_title'}|default:'Remove'}
  </button>
  <input type='hidden' name='confirm' 
               value='{session_get_flash var='confirm'}' />
{/form}
The admin controller action function is this:
  public function action_removeBook($book_id) {
    //return "action_removeBook: $book_id";
 
    $book = Model_Book::find($book_id);
 
    if (count($book->borrowers) > 0) {
      Session::set_flash('message', 'All copies must be returned.');
      return Response::redirect("/show/book/$book_id");
    }
 
    // using Input::param allows either get or post
    $confirm = Input::param('confirm');
 
    if (!$confirm) {
      // go back with message, button title change, and confirm setting
      Session::set_flash('confirm', 1);
      Session::set_flash('message', 'Are you sure? If so press remove again');
      Session::set_flash('button_title', "Confirm Remove");
      return Response::redirect("/show/book/$book_id");
    }
    $book->delete();
    //return "success";
    return Response::redirect('/');
  }

Return Book from User

Like modify and remove, this feature comes from the showBook script:

fuel/app/views/home/showBook.tpl
{if $borrowers}
{* one or more user with copy of the book *}
  {form attrs=['action' => "/admin/returnBookFromUser/{$book->id}", 
    'method'=>'get']}
  <button type="submit">Return from :</button>
    <select name="user_id">
      {* must have Model_User::__toString() defined to the name *}
      {html_options options=$borrowers}
    </select>
  {/form}
{/if}
The action function is this:
  public function action_returnBookFromUser($book_id) {
    $user_id = Input::param('user_id');
    //return "returnBookFromUser: $book_id, $user_id";
 
    $book = Model_Book::find($book_id);
    unset($book->borrowers[$user_id]);
    $book->quantity += 1;
    $book->save();
    //return "success";
 
    Session::set_flash('message', 'returned');
    return Response::redirect("/show/book/$book_id");
  }


© Robert M. Kline