The first thing we're going to implement 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->view("beer", "by_name", array( '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, 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 dataset, 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, 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 dataset and one to actually handle
the POST response. Take note that this demo
code is not really suited for production. You really want to add
validation here to make sure only valid data is stored - but it
should give you a solid idea on how to implement the basics with
Couchbase.
// 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, 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->set($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 backend 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 params 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')); // Define the Query options $options = array( 'limit' => INDEX_DISPLAY_LIMIT, // Limit the number of returned documents 'startkey' => $input, // Start the search at the given search input 'endkey' => $input . '\uefff' // End the search with a special character (see explanation below) ); // Query the view $results = $cb->view("beer", "by_name", $options); $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, 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 two new query parameters we make use of are:
startkey and endkey. They
allow us to define the key where the search should begin and the
key where the search should end. We use the special character
'\uefff' here which means "end". That way, we
only get results which correctly begin with the given search
string. 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. Very
straightforward.
For more information about using views for indexing and querying from Couchbase Server, here are some useful resources:
For technical details on views, how they operate, and how to write effective map/reduce queries, see Couchbase Server 2.0: Views and Couchbase Sever 2.0: Writing Views.
Sample Patterns: to see examples and patterns you can use for views, see Couchbase Views, Sample Patterns.
Timestamp Pattern: many developers frequently ask about extracting information based on date or time. To find out more, see Couchbase Views, Sample Patterns.