28
loading...
This website collects cookies to deliver better user experience
The complete code for this article is available in the GitHub repository devto-book-reviewers:googlebooks
composer require symfony/http-client
<?php
namespace App\Controller;
use SilverStripe\CMS\Controllers\ContentController;
use Symfony\Component\HttpClient\HttpClient;
class ReviewController extends ContentController
{
private static $allowed_actions = [
'index'
];
public function index()
{
$client = HttpClient::create();
$response = $client->request('GET', 'https://www.googleapis.com/books/v1/volumes?q=isbn:0747532699');
$data = $response->toArray();
return $data['items'][0]['volumeInfo']['title']; // "Harry Potter and the Philosopher's Stone"
}
}
getControllerName()
, then we instantiate a new page of this type in the admin interface and type the route in the URL field. (This is effectively how we created our Registration page in a previous article.)
./app/_config/
called routes.yml
and add the following:---
Name: approutes
After: framework/_config/routes#coreroutes
---
SilverStripe\Control\Director:
rules:
'review': 'App\Controller\ReviewController'
/review
, we will see Harry Potter and the Philosopher's Stone
in the browser. That's great! We know that HTTPClient
works and that the controller for this route is working. /review?q=Harry%20Potter
). We will need to add some bells and whistles to our controller for this to work. Thanks to SilverStripe, it will not be too hard to do this. We add a new argument to our index()
method of a HTTPRequest
type and access query parameters on it. If there is a valid query parameter we attach it to our API request.//...
public function index(HTTPRequest $request)
{
$q = $request->getVar('q');
if ($q) {
$client = HttpClient::create();
$response = $client->request('GET', 'https://www.googleapis.com/books/v1/volumes?q='.$q);
$data = $response->toArray();
return $data['items'][0]['volumeInfo']['title'];
}
return "Sorry, no valid query parameter found.";
}
//...
public function index(HTTPRequest $request)
{
$q = $request->getVar('q');
$langRestriction = $request->getVar('lang') ? "&langRestrict=" . $request->getVar('lang') : "";
if ($q) {
$client = HttpClient::create();
$response = $client->request('GET', 'https://www.googleapis.com/books/v1/volumes?q='. $q . $langRestriction);
$data = $response->toArray();
return $data['items'][0]['volumeInfo']['title'];
}
return "Sorry, no valid query parameter found.";
}
./app/src
called Service
. Inside this folder we will create a new file called GoogleBookParser.php
:<?php
namespace App\Service;
use SilverStripe\ORM\ArrayList;
class GoogleBookParser
{
public static function parse(array $item): array
{
$authors = $item['volumeInfo']['authors'] ?? [];
return [
'title' => $item['volumeInfo']['title'] ?? '',
'isbn' => $item['volumeInfo']['industryIdentifiers'][0]['identifier'] ?? '',
'volumeId' => $item['id'] ?? '',
'publishedDate' => $item['volumeInfo']['publishedDate'] ?? '',
'authors' => ($authors ? ArrayList::create(
array_map(function ($author) {
return ['AuthorName' => $author ?? ''];
}, $item['volumeInfo']['authors'])
) : ''),
'language' => $item['volumeInfo']['language'] ?? '',
'image' => $item['volumeInfo']['imageLinks']['thumbnail'] ?? '',
'pageCount' => $item['volumeInfo']['pageCount'] ?? '',
'categories' => ArrayList::create(
array_map(function ($category) {
return ['CategoryName' => $category ?? ''];
}, $item['volumeInfo']['categories'] ?? [])
),
'description' => $item['volumeInfo']['description'] ?? '',
];
}
public static function parseAll(array $response): array
{
return array_map(function ($item) {
return self::parse($item);
}, $response['items'] ?? []);
}
}
// Include the service
use App\Service\GoogleBookParser;
// ...
// inside the index-method:
$responseContent = $response->toArray();
$books = GoogleBookParser::parseAll($responseContent);
$book = $books[0];
return $book['title'];
// ...
<?php
namespace App\Controller;
use App\Service\GoogleBookParser;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\ORM\ArrayList;
use Symfony\Component\HttpClient\HttpClient;
class ReviewController extends ContentController
{
private static $allowed_actions = [
'index',
];
public function index(HTTPRequest $request)
{
$search = $request->getVar('q');
$searchQuery = "q=" . $search;
$startIndex = $request->getVar("startIndex") ?? 0;
$langRestriction = $request->getVar("langRestrict") ?? 'any';
$langRestrictionQuery = $langRestriction ? "&langRestrict=" . $langRestriction : "";
$maxResults = $request->getVar('maxResults') ?? 10;
$maxResultsQuery = '&maxResults=' . $maxResults;
// Get language codes
$client = HttpClient::create();
$response = $client->request('GET', 'https://gist.githubusercontent.com/jrnk/8eb57b065ea0b098d571/raw/936a6f652ebddbe19b1d100a60eedea3652ccca6/ISO-639-1-language.json');
$languageCodes = [["code" => "any", "name" => "Any"]];
array_push($languageCodes, ...$response->toArray());
$books = [];
$pagination = [];
if ($search) {
$basicQuery = $searchQuery
. $langRestrictionQuery
. $maxResultsQuery;
$response = $client->request('GET', 'https://www.googleapis.com/books/v1/volumes?'. $basicQuery);
$responseContent = $response->toArray();
$books = GoogleBookParser::parseAll($responseContent);
$pagination = $this->paginator('/review?' . $basicQuery, $responseContent['totalItems'], $startIndex, $maxResults);
$pagination['pages'] = ArrayList::create($pagination['pages']);
$pagination = ArrayList::create([$pagination]);
}
return $this->customise([
'Layout' => $this
->customise([
'Books' => ArrayList::create($books),
'Pagination' => $pagination,
'Query' => $search,
'Languages' => ArrayList::create($languageCodes),
'LangRestriction' => $langRestriction
])
->renderWith('Layout/Books'),
])->renderWith(['Page']);
}
/**
* Returns an array with links to pages with the necessary query parameters
*/
protected function paginator($query, $count, $startIndex, $perPage): array
{
$pagination = [
'start' => false,
'current' => false,
'previous' => false,
'next' => false,
'totalPages' => 0,
'pages' => false,
];
$totalPages = ceil($count / $perPage);
$currentPage = ceil($startIndex / $perPage) + 1;
$previousIndex = $startIndex - $perPage;
if ($previousIndex < 0) {
$previousIndex = false;
}
$nextIndex = $perPage * ($currentPage);
if ($nextIndex > $count) {
$nextIndex = false;
}
$pagination['start'] = [
'page' => $previousIndex > 0 ? 1 : false,
'link' => $previousIndex > 0 ? $query . '&startIndex=0' : false,
];
$pagination['current'] = [
'page' => $currentPage,
'link' => $query . '&startIndex=' . $startIndex
];
$pagination['previous'] = [
'page' => $previousIndex !== false ? $currentPage - 1 : false,
'link' => $previousIndex !== false ? $query . '&startIndex=' . $previousIndex : false,
];
$pagination['next'] = [
'page' => $nextIndex ? $currentPage + 1 : false,
'link' => $nextIndex ? $query . '&startIndex=' . $nextIndex : false,
];
$totalPages = ceil($count / $perPage);
$pagination['totalPages'] = $totalPages;
$pages = [];
for ($i = 0; $i < 3; $i++) {
$page = $currentPage + $i - 1;
if ($currentPage == 1) {
$page = $currentPage + $i;
}
if ($page > $totalPages) {
break;
}
if ($page < 1) {
continue;
}
$pages[] = [
'page' => $page,
'link' => $query . '&startIndex=' . ($page - 1) * $perPage,
'currentPage' => $page == $currentPage
];
$pagination['pages'] = $pages;
}
return $pagination;
}
}
index
method. This is because we want to use the same general structure as the rest of the site (which the Page
template stands for), and we want finer control over the layout for this particular page (which the Layout/Books
template stands for). Within the ->customise
method we are passing an array of variables to the template. Each key in this array can be accessed in the template by using the $
prefix. Let's go ahead and build that template now.Layout/Books
template, which will be included within the Page
template. If we look at the Page
template, as it is presented in the simple
theme, we have the following structure:<!-- ... -->
<div class="main" role="main">
<div class="inner typography line">
$Layout
</div>
</div>
<!-- ... -->
$Layout
is! Here's how we will do that. Create a new template in the templates/Layout
directory called Books.ss
. Give it the following structure:<section class="container">
<h1 class="text-center">Review a book</h1>
<% include SearchBar %>
<div id="Content" class="searchResults">
<% if $Books %>
<p class="searchQuery">Results for "$Query"</p>
<ul id="SearchResults">
<% loop $Books %>
<li>
<h4>
$title
</h4>
<div>
<% loop $authors %>
<p>$AuthorName</p>
<% end_loop %>
</div>
<a class="reviewLink" href="/review/book/{$volumeId}" title="Review "{$title}"">Review "{$title}"...</a>
</li>
<% end_loop %>
</ul>
<div id="PageNumbers">
<div class="pagination">
<% loop $Pagination %>
<span>
<% if $start.link %>
<a class="go-to-page" href="$start.link">|<</a>
<% end_if %>
<% if $previous.link %>
<a class="go-to-page" href="$previous.link"><</a>
<% end_if %>
<% loop $pages %>
<% if $currentPage %>
<strong><a class="go-to-page" href="$link">$page</a></strong>
<% else %>
<a class="go-to-page" href="$link">$page</a>
<% end_if %>
<% end_loop %>
<% if $next.link %>
<a class="go-to-page" href="$next.link">></a>
<% end_if %>
</span>
<% end_loop %>
</div>
</div>
<% end_if %>
</div>
</section>
include
statement that includes the SearchBar
template. This is a partial template that we will create in the templates/Includes
directory. Call it SearchBar.ss
.<form class="Actions">
<div class="line">
<div class="field">
<label for="search-input">Search</label>
<input id="search-input" type="text" name="q" class="text" value="$Query">
</div>
<% if $Languages %>
<div class="field">
<label for="langRestrict">Language</label>
<select name="langRestrict" id="langRestrict">
<% loop $Languages %>
<option value=$code <% if $Up.LangRestriction == $code %>selected<% end_if %>>$name</option>
<% end_loop %>
</select>
</div>
<% end_if %>
</div>
<div class="line">
<div class="field">
<input type="submit" class="btn" value="Search" />
</div>
</div>
</form>
$Up
variable? This is part of the SilverStripe template syntax to access variables that are one step above the current scope. Whenever we are within a loop-structure we are also one step removed from the variables that were passed to the template. LangRestriction
is one of the variables that we passed to the template, and Up.LangRestriction
is the variable that we can access from within the loop. $code
and $name
variables. These are keys on elements of each of the $Languages
array. In the controller we had $languageCodes = [["code" => "any", "name" => "Any"]];
. So the first element of the $Languages
array would have $code
set to any
and $name
set to Any
. It's the same logic that goes into explaining the variable uses in the Books.ss
template./review
URL and see the results.