1. Introduction
    1. About Agavi
    2. MVC in Agavi
    3. Overview of Agavi
    4. Overview of Application Execution Flow
    5. A Word About Actions
    6. Application filesystem layout
    7. Overview of application configuration
  2. Setting Up The Initial Application
    1. Installing Agavi
    2. Creating an Agavi Project
    3. Finishing The Setup
    4. Finishing The Basic Setup
    5. Installing a New Copy of Your Application
  3. Adding First Code
    1. Creating A New module
    2. Creating A New Action
    3. Tying Things Together — An Introduction To Routing
    4. Fixing The Bloggie Routing
    5. Accessing Request Parameters and Validation Basics
    6. Handling Validation Errors
  4. Putting The M in MVC
    1. Creating A New Model
    2. Adapting The Actions and Views
    3. Custom Validators
  5. Polishing It Up
    1. Layers and Layouts
    2. Applying Our Layout
    3. What Are Slots?
    4. Adding The Post's Title To The URL
    5. Routing Callbacks
    6. Using Callbacks for the Title in URLs
  6. Connecting to a database
    1. The Database Manager
  7. Handling Output Variants
    1. Output Types
    2. Exception Templates
    3. Generating an RSS Feed
  8. Form Processing
    1. Adding a Post
    2. Editing a Post
    3. The Form Population Filter (FPF)
  9. Creating a User Authentication System
  10. Adding To The Master Template

Adding a Post

First we need to create a new action, let's name it "Add". We'll need three views, "Input" to display the empty form, "Error" in case any validation or other error occurs and "Success" for handling any successful add to the database.

bloggie$ dev/bin/agavi action-wizard

Module name: Posts

Action name: Add

Space-separated list of views to create for Add [Success]: Input Success Error

bloggie$

This created the action and all three views for us. Next we'll add the route so we can reach the action:

<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- matches .rss at the end of the url, strips it, sets the output type to "rss" and continues matching -->
      <route name="ot_rss" pattern=".rss$" cut="true" stop="false" output_type="rss" />
      
      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Posts" action="%actions.default_action%" />

      <route name="posts" pattern="^/posts" module="Posts">

        <route name=".create" pattern="^/add$" action="Add" />

        <route name=".post" pattern="^/(post:\d+)(-{title:[-\w]+})?" action="Post">
          <callbacks>
            <callback class="PostRoutingCallback" />
          </callbacks>
        
          <route name=".show" pattern="^$" action=".Show" />

        </route> 
      
      </route>

    </routes>
  </ae:configuration>
</ae:configurations>

Now we need to create the input form in app/modules/Posts/templates/AddInput.php. We'll leave the posted date out of the form as we'll add that in the code (we do know when the post was added) and the author - this should be handled via authentication later:

<form action="<?php echo $t['target_url']; ?>" method="post">
  <fieldset>
    <div class="form_row">
      <label for="input_title">Title:</label>
      <input type="text" name="title" id="input_title" />
    </div>
    <div class="form_row">
      <label for="input_content">Content:</label>
      <textarea name="content" id="input_content"></textarea>
    </div>
    <div class="form_row">
      <label for="input_category">Category:</label>
      <select name="category" id="input_category">
        <option value="1">No category</option>
        <option value="2">Agavi</option>
      </select>
    </div>
    <div class="form_row form_row_submit">
      <button type="submit" class="submit">Add Post</button>
    </div>
  </fieldset>
</form>

and adapt the app/modules/Posts/views/AddInputView.class.php to generate the submit url for us:

<?php

class Posts_AddInputView extends BlogPostsBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);

    $ro = $this->getContext()->getRouting();

    $this->setAttribute('target_url', $ro->gen('posts.create'));
    $this->setAttribute('_title', 'Add a new Post');
  }
}

?>

Let's try that. Point your browser to http://localhost/bloggie/pub/posts/add. Hmm, blank page. Well, the actions getDefaultViewName() method returns "Success" as default view and that's what's getting called - and that's why we don't see the form. So let's go and adapt that in app/modules/Posts/actions/AddAction.class.php:

<?php

class Posts_AddAction extends BlogPostsBaseAction
{
  /**
   * Returns the default view if the action does not serve the request
   * method used.
   *
   * @return     mixed <ul>
   *                     <li>A string containing the view name associated
   *                     with this action; or</li>
   *                     <li>An array with two indices: the parent module
   *                     of the view to be executed and the view to be
   *                     executed.</li>
   *                   </ul>
   */
  public function getDefaultViewName()
  {
    return 'Input';
  }
}

?>

And now we see our form, as expected.

Validating The Form Data

A usual we need to validate the incoming form data. Let's list our restrictions:


  • The title must be a string and must not be longer than 255 characters or it would not fit in our database field.
  • The content must be a string not longer than 65536 characters as this is the maximum size for the mysql text column.
  • The category field must have a valid id.
  • All fields are required.

Let's forget about the fact that the categories are stored in the database and should thus be validated accordingly and just assume that for the moment there's only two categories with the ids 1 and 2. We'll be using an AgaviInarrayValidator for the categories field and an AgaviStringValidator for the title and content fields. So our validation file in app/modules/Posts/validate/Add.xml would look like this:

<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Posts/config/validators.xml"
>
  <ae:configuration>

    <validators>
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <errors>
          <error>The title field has an invalid value.</error>
          <error for="required">Please provide a title.</error>
          <error for="max_error">The title must be shorter than 255 characters.</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="max">255</ae:parameter>
        </ae:parameters>
      </validator>

      <validator class="string">
        <arguments>
          <argument>content</argument>
        </arguments>
        <errors>
          <error>The content field has an invalid value.</error>
          <error for="required">Please provide a post body.</error>
          <error for="max_error">The post body must be shorter than 65536 characters.</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="max">65536</ae:parameter>
        </ae:parameters>
      </validator>

      <validator class="inarray">
        <arguments>
          <argument>category</argument>
        </arguments>
        <errors>
          <error>Please choose a valid category.</error>
          <error for="required">Please choose a category.</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="values">
            <ae:parameters>
              <ae:parameter>1</ae:parameter>
              <ae:parameter>2</ae:parameter>
            </ae:parameters>
          </ae:parameter>
        </ae:parameters>
      </validator>
    </validators>

  </ae:configuration>
</ae:configurations>

Don't forget that validation only happens if the target action handles the request method. So we'll have to add at least a stub executeWrite() method to our action:

<?php

class Posts_AddAction extends BlogPostsBaseAction
{
  /**
   * Serves Write (POST) requests.
   * 
   * @param      AgaviRequestDataHolder the incoming request data
   * 
   * @return     mixed <ul>
   *                     <li>A string containing the view name associated
   *                     with this action; or</li>
   *                     <li>An array with two indices: the parent module
   *                     of the view to be executed and the view to be
   *                     executed.</li>
   *                   </ul>
   */
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    return 'Success';
  }
  
  /**
   * Returns the default view if the action does not serve the request
   * method used.
   *
   * @return     mixed <ul>
   *                     <li>A string containing the view name associated
   *                     with this action; or</li>
   *                     <li>An array with two indices: the parent module
   *                     of the view to be executed and the view to be
   *                     executed.</li>
   *                   </ul>
   */
  public function getDefaultViewName()
  {
    return 'Input';
  }
}

?>

Handling Validation Errors

The last time we had to handle validation erros was when we forwarded to the 404 page when an invalid post-id was found in the url to the posts detail page. At that time, a forward was the right way to go but in this case we want to re-display the form, fill in the data the user provided and insert error message where appropriate. All we need to do is re-display our form on the error page and the AgaviFormPopulationFilter will perform all of those duties. To re-display the form we could either include it in the error views template or we could set the input template in the view. For now we'll just go include the input template in the error template. However, we need to provide the target URL from the error view as well and while we're at it, let's set a proper title:

<?php

class Posts_AddErrorView extends BlogPostsBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);

    $ro = $this->getContext()->getRouting();
    
    $this->setAttribute('target_url', $ro->gen('posts.create'));
    $this->setAttribute('_title', 'Add a new Post - Error');
  }
}

?>

Go ahed, try submitting a post with an empty title but fill in some content. You'll see the message "Please provide a title." right underneath the title field and the text you provided for the content is already filled in. Things could use a little styling but the functionality is there. So how did this work? In short words: The AgaviFormPopulationFilter (often called FPF) parse the HTML code and finds the form that was submitted. It then inserts the values provided by the user and the error messages at the appropriate place. We'll see that in more detail a little later, for now just enjoy that you didn't have to write a single line of code for that.

Storing The New Post

Doing this really properly would require a whole set of new models, we'll postpone that to a later chapter so that we can keep this chapter within reasonable bounds. For now we'll deal with raw ids for the category and the user and format the posted date manually - it will still get the post stored. We still need to add a set of new properties to the app/modules/Posts/models/PostModel.class.php:


  • An author-id
  • A category-id
  • The respective getters and setters
<?php

class Posts_PostModel extends BlogPostsBaseModel
{
  ...
  private $categoryId;
  private $authorId;
  ...
 
  public function getCategoryId()
  {
    return $this->categoryId;
  }
  
  public function setCategoryId($id)
  {
    $this->categoryId = $id;
  }
  
  ...
  
  public function getAuthorId()
  {
    return $this->authorId;
  }
  
  public function setAuthorId($id)
  {
    $this->authorId = $id;
  }
  
  ...
}

?>

Adding the new properties to the toArray() and fromArray() methods proves a little tedious though. We have take into account that we'll be passing partial array sometimes - especially when adding a post, so we'd have to surround each element with a condition that checks whether the key is set in the incoming array - this makes the whole code bloat. So let's go an introduce an array that lists how to map incoming data to getters/setters and use that one:

<?php

class Posts_PostModel extends BlogPostsBaseModel
{
  private $id;
  private $title;
  private $posted;
  private $categoryId;
  private $categoryName;
  private $authorId;
  private $authorName;
  private $content;
  
  protected $fields = array(
    'id' => 'Id',
    'title' => 'Title',
    'posted' => 'Posted',
    'category_id' => 'CategoryId',
    'category_name' => 'CategoryName',
    'author_id' => 'AuthorId',
    'author_name' => 'AuthorName',
    'content' => 'Content',
  );
  
....
  
  public function fromArray(array $data)
  {
    foreach($data as $key => $value) {
      if(isset($this->fields[$key])) {
        $setter = 'set' . $this->fields[$key];
        $this->$setter($value);
      }
    }
  }
  
  public function toArray()
  {
    $data = array();

    foreach($this->fields as $key => $getter) {
      $getter = 'get'.$getter;
      $data[$key] = $this->$getter();
    }
    
    return $data;
  }
}

?>

While this does puts some contraints on the naming of getters and setters, it works like a charm and reduces code massively. The next thing we'll need to do is teach our Posts_PostManagerModel in app/modules/Posts/Models/PostManager.class.php how to store a new post:

<?php

class Posts_PostManagerModel extends BlogPostsBaseModel
{

  ...

  public function storeNew(Posts_PostModel $post)
  {
    $con = $this->getContext()->getDatabaseManager()->getDatabase()->getConnection();
    
    $sql = 'INSERT INTO
  posts
(
  title,
  category_id,
  content,
  posted,
  author_id
)
VALUES
(
  ?,
  ?,
  ?,
  NOW(),
  ?
)';

    $stmt = $con->prepare($sql);
    
    $stmt->bindValue(1, $post->getTitle(), PDO::PARAM_STR);
    $stmt->bindValue(2, $post->getCategoryId(), PDO::PARAM_INT);
    $stmt->bindValue(3, $post->getContent(), PDO::PARAM_STR);
    $stmt->bindValue(4, $post->getAuthorId(), PDO::PARAM_INT);
    
    $stmt->execute();
    
    return $con->lastInsertId();
  }
  
}

?>

Now we adapt the action to store the post - we retrieve the post from the database after storing it to get the default values added by the database itself.

<?php

class Posts_AddAction extends BlogPostsBaseAction
{
  /**
   * Serves Write (POST) requests.
   * 
   * @param      AgaviRequestDataHolder the incoming request data
   * 
   * @return     mixed <ul>
   *                     <li>A string containing the view name associated
   *                     with this action; or</li>
   *                     <li>An array with two indices: the parent module
   *                     of the view to be executed and the view to be
   *                     executed.</li>
   *                   </ul>
   */
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    $ctx = $this->getContext();
    
    $data = array(
      'title' => $rd->getParameter('title'),
      'content' => $rd->getParameter('content'),
      'category_id' => $rd->getParameter('category'),
      'author_id' => 1, // let's bind that to a fixed value for the moment
    );
    
    $post = $ctx->getModel('Post', 'Posts', array($data));
    $postManager = $ctx->getModel('PostManager', 'Posts');
    
    $postId = $postManager->storeNew($post);
    
    // we need a post with at least and id and a title to create an url
    // so we reload the post from the database
    
    $post = $postManager->retrieveById($postId);
    
    $this->setAttribute('post', $post);
    
    return 'Success';
  }
  
  /**
   * Returns the default view if the action does not serve the request
   * method used.
   *
   * @return     mixed <ul>
   *                     <li>A string containing the view name associated
   *                     with this action; or</li>
   *                     <li>An array with two indices: the parent module
   *                     of the view to be executed and the view to be
   *                     executed.</li>
   *                   </ul>
   */
  public function getDefaultViewName()
  {
    return 'Input';
  }
}

?>

Redirecting on Success

After we've successfully stored the post we should redirect to a new page - we could have a completely new page saying "Thanks, your post has been added." but the posts detail page will serve our purpose just as well. So let's add the redirect to app/modules/Posts/views/AddSuccessView.class.php:

<?php

class Posts_AddSuccessView extends BlogPostsBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $url = $this->getContext()->getRouting()->gen('posts.post.show', array('post' => $this->getAttribute('post')));
    
    $this->getResponse()->setRedirect($url);
  }
}

?>

And with this, we're done. We could add some buttons but we'll to that later when we've added some security so that only admins can access the page.