A newer version of this documentation is available.

View Latest

Managing beers

First task is to show a list of beers in a table.

The table itself will contain the name of the beer and links to the brewery as well as buttons to edit or delete the beer. We’ll implement interactive filtering on the table later as well. The following code should be inserted after the / action to keep everything in order.

$app->get('/beers', function() use ($app, $cb) {
    // Load all beers from the beer/by_name view
    $results = $cb->query(
        CouchbaseViewQuery::from("beer", "by_name")
            ->limit(INDEX_DISPLAY_LIMIT)
    );

    $beers = array();
    // Iterate over the returned rows
    foreach($results['rows'] as $row) {
        // Load the full document by the ID
        $doc = $cb->get($row['id']);
        if($doc) {
            // Decode the JSON string into a PHP array
            $doc = json_decode($doc->value, true);
            $beers[] = array(
                'name' => $doc['name'],
                'brewery' => $doc['brewery_id'],
                'id' => $row['id']
            );
        }

    }

    // Render the template and pass on the beers array
    return $app['twig']->render('beers/index.twig.html', compact('beers'));
});

We’re making use of our previously defined view beer/by_name. We also pass in a limit option to make sure we don’t load all documents returned by the view. The results variable stores the view response and contains the actual data inside the rows element. We can then iterate over the data set, but since the view only returns the document ID and we need more information, we fetch the full document through the get() method. If it actually finds a document by the given ID, we convert the JSON string to a PHP array and add it to the list of beers. The list is then passed on to the template to display it.

The corresponding template beers/index.twig.html looks like this:

{% extends "layout.twig.html" %}

{% block content %}
<h3>Browse Beers</h3>

<form class="navbar-search pull-left">
  <input id="beer-search" type="text" class="search-query" placeholder="Search for Beers">
</form>

<table id="beer-table" class="table table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th>Brewery</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
      {% for beer in beers %}
        <tr>
          <td><a href="/beersample-php/beers/show/{{beer.id}}">{{beer.name}}</a></td>
          <td><a href="/beersample-php/breweries/show/{{beer.brewery}}">To Brewery</a></td>
          <td>
            <a class="btn btn-small btn-warning" href="/beersample-php/beers/edit/{{beer.id}}">Edit</a>
            <a class="btn btn-small btn-danger" href="/beersample-php/beers/delete/{{beer.id}}">Delete</a>
          </td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
{% endblock %}

Aside from normal HTML table markup, we make use of the {% for beer in beers %} block to loop over the beers array. We then print out each row and show the name of the beer, the link to the brewery and also buttons to edit and delete the beer. We’ll implement these methods in a minute.

The next action we’re going to implement is the show action. When you click on a beer, it should display all attributes from the JSON document in a table so we can inspect them properly. Since everything is stored in one document, we just need to fetch it by the given ID, decode it from the JSON string and pass it on to the view. Very straightforward and performant:

$app->get('/beers/show/{id}', function($id) use ($app, $cb) {
    // Get the beer by its ID
    $beer = $cb->get($id);
    if($beer) {
       // If a document was found, decode it
       $beer = json_decode($beer->value, true);
       $beer['id'] = $id;
    } else {
       // Redirect if no document was found
       return $app->redirect('/beers');
    }

    // Render the template and pass the beer to it
    return $app['twig']->render(
        'beers/show.twig.html',
        compact('beer')
    );
});

The template iterates over the JSON attributes and prints their name and value accordingly. Note that some documents can contain nested values which is not covered here.

{% extends "layout.twig.html" %}

{% block content %}
<h3>Show Details for Beer "{{beer.name}}"</h3>
<table class="table table-striped">
    <tbody>
       {% for key,attribute in beer %}
        <c:forEach items="${beer}" var="item">
            <tr>
                <td><strong>{{key}}</strong></td>
                <td>{{attribute}}</td>
            </tr>
          </c:forEach>
          {% endfor %}
    </tbody>
</table>
{% endblock %}

The next action we’re going to implement is the delete action.

$app->get('/beers/delete/{id}', function($id) use ($app, $cb) {
    // Delete the Document by its ID
    $cb->delete($id);
    // Redirect to the Index action
    return $app->redirect('/beersample-php/beers');
});

As you can see, the delete call is very similar to the previous get method. After the document has been deleted, we redirect to the index action. If we’d like to, we could get more sophisticated in here. For example, good practice would be to fetch the document first and check if the document type is beer to make sure only beers are deleted here. Also, it would be appropriate to return a error message if the document didn’t exist previously. Note that there is no template needed because we redirect immediately after deleting the document.

Since we can now show and delete beers, its about time to make them editable as well. We now need to implement two different actions here. One to load the data set and one to actually handle the POST response. Take note that this demo code is not really suited for production, but it should give you a solid idea on how to implement the basics with Couchbase. In a production app, you need to add validation here to make sure only valid data is stored.

// Show the beer form
$app->get('/beers/edit/{id}', function($id) use ($app, $cb) {
    // Fetch the document
    $beer = $cb->get($id);
    if($beer) {
        // Decode the document
       $beer = json_decode($beer->value, true);
       $beer['id'] = $id;
    } else {
        // Redirect if no document was found
       return $app->redirect('/beers');
    }

    // Pass the document on to the template
    return $app['twig']->render(
        'beers/edit.twig.html',
        compact('beer')
    );
});

// Store submitted Beer Data (POST /beers/edit/<ID>)
$app->post('/beers/edit/{id}', function(Request $request, $id) use ($app, $cb) {
    // Extract the POST form data out of the request
    $data = $request->request;

    $newbeer = array();
    // Iterate over the POSTed fields and extract their content.
    foreach($data as $name => $value) {
        $name = str_replace('beer_', '', $name);
        $newbeer[$name] = $value;
    }

    // Add the type field
    $newbeer['type'] = 'beer';

    // Encode it to a JSON string and save it back
    $cb->upsert($id, json_encode($newbeer));

    // Redirect to show the beers details
    return $app->redirect('/beersample-php/beers/show/' . $id);
});

The missing link between the GET and POST handlers is the form itself. The template is called edit.twig.html and looks like this:

{% extends "layout.twig.html" %}

{% block content %}
<h3>Edit Beer</h3>

<form method="post" action="/beersample-php/beers/edit/{{beer.id}}">
    <fieldset>
      <legend>General Info</legend>
      <div class="span12">
        <div class="span6">
          <label>Name</label>
          <input type="text" name="beer_name" placeholder="The name of the beer." value="{{beer.name}}">

          <label>Description</label>
          <input type="text" name="beer_description" placeholder="A short description." value="{{beer.description}}">
        </div>
        <div class="span6">
          <label>Style</label>
          <input type="text" name="beer_style" placeholder="Bitter? Sweet? Hoppy?" value="{{beer.style}}">

          <label>Category</label>
          <input type="text" name="beer_category" placeholder="Ale? Stout? Lager?" value="{{beer.category}}">
        </div>
      </div>
    </fieldset>
    <fieldset>
        <legend>Details</legend>
        <div class="span12">
            <div class="span6">
              <label>Alcohol (ABV)</label>
              <input type="text" name="beer_abv" placeholder="The beer's ABV" value="{{beer.abv}}">

              <label>Biterness (IBU)</label>
              <input type="text" name="beer_ibu" placeholder="The beer's IBU" value="{{beer.ibu}}">
            </div>
            <div class="span6">
              <label>Beer Color (SRM)</label>
              <input type="text" name="beer_srm" placeholder="The beer's SRM" value="{{beer.srm}}">

              <label>Universal Product Code (UPC)</label>
              <input type="text" name="beer_upc" placeholder="The beer's UPC" value="{{beer.upc}}">
            </div>
        </div>
    </fieldset>
    <fieldset>
        <legend>Brewery</legend>
        <div class="span12">
            <div class="span6">
              <label>Brewery</label>
              <input type="text" name="beer_brewery_id" placeholder="The brewery" value="{{beer.brewery_id}}">
            </div>
        </div>
    </fieldset>
    <div class="form-actions">
        <button type="submit" class="btn btn-primary">Save changes</button>
    </div>
</form>
{% endblock %}

The only special part in the form are the Twig blocks like {{beer.brewery_id}}. They allow us to easily include the actual value from the field (when there is one). You can now change the values in the input fields, hit Save changes and see the updated document either in the web application or through the Couchbase Admin UI.

There is one last thing we want to implement here. You may have noticed that the index page lists all beers but also has a search box on the top. Currently, it won’t work because the back end is not yet implemented. The JavaScript is already in place in the assets/js/beersample.js file, so look through it if you are interested. It just does an AJAX request against the server with the given search value, expects a JSON response and iterates over it while replacing the original table rows with the new ones.

We need to implement nearly the same view code as in the index action, but this time we make use of two more view query parameters that allow us to only return the range of documents we need:

$app->get('/beers/search', function(Request $request) use ($app, $cb) {
    // Extract the search value
    $input = strtolower($request->query->get('value'));

    // Query the view
    $q = CouchbaseViewQuery::from('beer', 'by_name')
        ->limit(INDEX_DISPLAY_LIMIT)
        ->range($input, $input . '\uefff');
    $results = $cb->query($q);

    $beers = array();
    // Iterate over the resulting rows
    foreach($results['rows'] as $row) {
        // Load the corresponding document
        $doc = $cb->get($row['id']);
        if($doc) {
            // If the doc is found, decode it.
            $doc = json_decode($doc->value, true);
            $beers[] = array(
                'name' => $doc['name'],
                'brewery' => $doc['brewery_id'],
                'id' => $row['id']
            );
        }

    }
    // Return a JSON formatted response of all beers for the JavaScript code.
    return $app->json($beers, 200);
});

The limit method specifies the maximum number of documents to return. The range method defines start and end keys for the search. The special character concatenated to the end key, '\uefff' , means "end." Using this character assures that only documents beginning with the given search string are returned. This is a little trick that comes in very handy from time to time.

The rest is very similar to the index action so we’ll skip the discussion for that. Also, we don’t need a template here because we can return the JSON response directly.