Building a Web App With Symfony 2: Development
In Part 1, I have shown you how to set up Symfony 2 and link up the database. We also covered some fundamental concepts of the framework. In this part, we will link things up by creating routes, controllers, entities/repositories, and views to get the site up and running.
Routes
Our instance of Symfony uses the YAML format to configure the routes for the application. A sample section of this site's routes is shown below:
File location: src/tr/rsywxBundle/Resources/config/routing.yml
home:
pattern: /
defaults: { _controller: trrsywxBundle:Default:index }
contact:
pattern: /contact
defaults:
_controller: FrameworkBundle:Template:template
template: 'trrsywxBundle:Default:contact.html.twig'
book_list:
pattern: /books/list/{page}/{key}
defaults:
page: 1
key: null
_controller: trrsywxBundle:Book:list
books_search:
pattern: /books/search
defaults: {_controller: trrsywxBundle:Book:search}
requirements:
_method: POST
book_detail:
pattern: /books/{id}.html
defaults: { _controller: trrsywxBundle:Book:detail}
Every app needs an entry point. This is the "home" route. pattern
defines the URI pattern the route should match. As this is the entry point, /
is used. defaults:_controller
defines the action the application will take when this route is matched. Please note the FQN format it used to map the route (and the pattern) for the action to take. In this case, it means whenever we are entering the site with just its domain, the index
action in the Default
controller under the namespace trrsywxBundle
will be triggered.
There are other parameters you can set in the pattern and in the defaults. For example, the book_list
route has a pattern with 2 parameters in the URI: page
means the current page when there is more than one page for the result to be displayed (I will cover the Pagination in Part 3) and key
is the keyword used to search books matching that key
(in my current implementation, I only search the beginning of the title of a book). All parameters in the pattern
must be within a pair of curly braces.
In book_list:default
, I give the default values for the above two parameters: 1 for page
and null for key
. By doing so, a simple URI like http://localhost/app_dev.php/books/list
will list the 1st page of all books, while http://localhost/app_dev.php/books/list/3/Beauty
will simply list the 3rd page of all books whose title starts with Beauty
(like "Beauty and Beast").
You will also notice that the books_search
route has requirements:_method
set to POST
. This restricts the calling method for that pattern to be just POST (in a form submission) as I don't want the users to simply visit /books/search
in the browser. In my implementation, this route is meant for internal usage and should redirect to another page with the search result matching the criteria submitted (POSTed)via a form.
The full documentation on routes can be found on Symfony's site.
Controllers
The next step is to define controllers. I have a total of 4 controllers located in src/tr/rsywxBundle/Controller
. Namely, they are: BookController.php, DefaultController.php, LakersController.php, and ReadingController.php. I group the functions related to various routes based on their functionality.
I will show just two controllers here, matching book_list
and books_search
.
<?php
class BookController extends Controller
{
// ... Many other functions here, see source
public function listAction($page, $key)
{
$em = $this->getDoctrine()->getManager(); // Get the Entity Manager
$rpp = $this->container->getParameter('books_per_page'); // Get the global parameter for how many books to show on one page
$repo = $em->getRepository('trrsywxBundle:BookBook'); // Get the repository
list($res, $totalcount) = $repo->getResultAndCount($page, $rpp, $key); // Get the result
$paginator = new \tr\rsywxBundle\Utility\Paginator($page, $totalcount, $rpp); // Init the paginator
$pagelist = $paginator->getPagesList(); // Get the pagelist used for navigation
return $this->render('trrsywxBundle:Books:List.html.twig', array('res' => $res, 'paginator' => $pagelist, 'cur' => $page, 'total' => $paginator->getTotalPages(), 'key'=>$key)); // Render the template using necessary parameters
}
public function searchAction(Request $req)
{
$q = $req->request->all(); // Get the posted data
$page = 1; // Get which page to display
$key = $q['key']; // Get the search criteria
$em = $this->getDoctrine()->getManager();
$rpp = $this->container->getParameter('books_per_page');
$repo = $em->getRepository('trrsywxBundle:BookBook');
list($res, $totalcount) = $repo->getResultAndCount($page, $rpp, $key);
$paginator = new \tr\rsywxBundle\Utility\Paginator($page, $totalcount, $rpp);
$pagelist = $paginator->getPagesList();
return $this->render('trrsywxBundle:Books:List.html.twig', array('res' => $res, 'paginator' => $pagelist, 'cur' => $page, 'total' => $paginator->getTotalPages(), 'key' => $key));
}
}
In a typical controller fashion, it does three things:
- Get all the preparation work done (input parameters, get the Entity Manager, Repository, etc);
- Get the results from a repository;
- Display the results (with or without further processing) by rendering a template (Symfony uses Twig as its template engine).
Observe three things carefully:
-
Look at how the parameters we defined in route
book_list
(page
andkey
) are passed into the function call by name. Symfony does not care about the order of the parameters' appearance but requires a strict name match. -
Look at how the POSTed parameters in a form are passed into
searchAction
and how we retrieve the necessary information from the submitted data. -
getParameter
is a function to retrieve global parameters. The global parameters are defined in the fileapp/config/parameters.yml.dist
(autogenerated with composer into .yml) and look like this:
books_per_page: 10
Normally, it is good practice to leave the application logic in the controller functions and the data provider in a repository. This helps organize the code and keep it re-usable.
A detailed document on Controllers can be found here.
Entities and Repositories
In ORM's methodlogy, an entity is the objective reflection of a database table. Instead by issuing native SQL commands in your PHP to manipulate the data, we can use intuitive and straightforward ways to CRUD the data. While the entities generated by Symfony contain simple methods to retrieve a data by ID, in a proper application that is far from enough. Repositories are there to provide more customized ways to manipulate data.
Entities are generated via a console/terminal command: php app\console doctrine:generate:entity
(See Part 1 or the official documentation.) The generated PHP entity files are located at src/tr/rsywxBundle/Entity
. Take a look at those PHP files to understand more on how the database tables are mapped (via ORM) into a PHP class.
Note: Don't make any changes to these PHP files directly. They are meant to be generated by Symfony.
To create a repository to hold all the customized database manipulation methods, you need to do two things:
- Modify corresponding ORM mapping files (located at src/tr/rsywxBundle/Resources/config/doctrine) to specify a repository class for that database object;
- Create all necessary functions to provide required database manipulation capabilities (like retrieving data).
Take my books collection table (and its entity for example):
File location: src/tr/rsywxBundle/Resources/config/doctrine/BookBook.orm
tr\rsywxBundle\Entity\BookBook:
type: entity
repositoryClass: tr\rsywxBundle\Entity\BookRepository # Add this line to indicate that we will use BookRepository.php as the repository class for BookBook table
table: book_book
fields:
...
After that, run php app\console doctrine:generate:entity
again. You will notice a new BookRepository.php
file is created under src/tr/rsywxBundle/Entity
. You can now open that file and create your own functions for data manipulation:
public function getResultAndCount($page, $rpp, $key=null)
{
// Get the book count
$em = $this->getEntityManager();
if ($key == 'null')
{
$q1 = $em->createQuery('select count(b.id) bc from trrsywxBundle:BookBook b');
}
else
{
$qstr = sprintf("select count(b.id) bc from trrsywxBundle:BookBook b where b.title like '%s%%'", $key);
$q1 = $em->createQuery($qstr);
}
$res1 = $q1->getSingleResult();
$count = $res1['bc'];
// Now get the wanted result specified by page
$repo = $em->getRepository('trrsywxBundle:BookBook');
$q2 = $repo->createQueryBuilder('r')
->setMaxResults($rpp)
->setFirstResult(($page - 1) * $rpp)
->orderBy('r.id', 'desc');
if ($key <> 'null')
{
$key = $key . '%';
$q2->where('r.title like :key')
->setParameter('key', $key);
}
$q2 = $q2->getQuery();
$res2 = $q2->getResult();
return array($res2, $count);
}
In the code above, which is a very representative code segment for retrieving data, I have used 2 ways to generate an SQL query and execute it.
One is to use $em->createQuery()
, which uses a similar grammar as we will use when we issue an SQL command in a database. The only difference is in the from
segment. Instead of using a raw table name (book_book
), we use the table class name (trrsywxBundle:BookBook
).
getSingleResult
is used in my first query as I am expecting there will be only one result returned from this query (the count of books matching a certain criteria or ALL books).
In the 2nd query, I used createQueryBuilder
and chained all the methods to set necessary SQL parameters (the starting record, the record count, the order, and a where
condition to filter).
getResult
is used since we will be expecting more than one record.
Finally, we pack the results into an array: $res2
as the resulted dataset and $count
as the count of the resulted dataset. They will be further processed in the calling controller function (in this case, create a Paginator object to facilitate the navigation).
Views and Templates
So far we have been doing the "background" work. Nothing is presentable without a view. In Symfony, as in most frameworks, a view is equivalent to a template. As I mentioned earlier, Symfony uses Twig as its template engine. It is simple to learn and quick to display.
I will show you a segment of the Twig template I used to display the Book List page.
(I am not a good web designer, so I slap on Bootstrap 3.0 to quickly bootstrap my page design. You can use your own design or ready-made templates).
Note: below is just a fragment of the whole template.
File location: src/tr/rsywxBundle/Resources/views/Books/List.html.twig
{% set active=2 %}
{% extends 'trrsywxBundle:Default:index.html.twig' %}
{% block title %}<title>{{ "RSYWX | Book Collection | Page }}{{cur}}</title>{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
<h3>My book collection</h3>
</div>
<div class="col-md-6">
<h3>Page {{cur}} of {{total}}</h3>
</div>
<div class="col-md-4">
<form class="form-inline" method="post" action="{{path('books_search')}}" name="searchform" id="searchform">
<div class="form-group">
<input type="text" class="form-control" required="required" placeholder="Search the beginning of a book title" name="key" id="key" value="{{key}}"/>
<input type="hidden" name="cur" id="cur" value="{{cur}}"/>
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
<br>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<tbody>
<tr>
<td><strong>ID</strong></td>
<td><strong>Title</strong></td>
<td><strong>Author</strong></td>
<td><strong>Purchase Date</strong></td>
<td><strong>Location</strong></td>
</tr>
{% for book in res %}
{% set author=book.author%}
{%if author ==''%}
{%set author='(anonymous)'%}
{%endif%}
<tr>
<td><a href="{{path('book_detail', {'id':book.id})}}">{{book.id}}</a></td>
<td><a href="{{path('book_detail', {'id':book.id})}}">{{book.title}}</a></td>
<td>{{author}}</td>
<td>{{book.purchdate|date('Y-m-d')}}</td>
<td>{{book.location}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="pager">
<ul>
<li class="previous"><a href="{{path('book_list', {'page':1, 'key':key})}}">First</a></li>
{%if cur==1%}
<li class="previous disabled"><a href="{{path('book_list', {'page':cur-1, 'key':key})}}">Previous</a></li>
{%else%}
<li class="previous"><a href="{{path('book_list', {'page':cur-1, 'key':key})}}">Previous</a></li>
{%endif%}
{%if cur==total%}
<li class="previous disabled"><a href="{{path('book_list', {'page':cur, 'key':key})}}">Next</a></li>
{%else%}
<li class="previous"><a href="{{path('book_list', {'page':cur+1, 'key': key})}}">Next</a></li>
{%endif%}
<li class="previous"><a href="{{path('book_list', {'page':total, 'key':key})}}">Last</a></li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
A few things to discuss here:
- To display something, use
{{obj.member}}
notation; to control the flow or manipulate data, use{% control statement or manipulation statement %}
. Actually, these are the two and only two grammar structures in Twig. {% extends %}
can be used to extend the page layout from a parent (base) layout. It is very useful for page design.{% block title %}...{% endblock %}
replaces the content in the parent layout with your own content.{{path(...)}}
is used to generate URIs matching that route in the template. It is a very important helper function that everyone will use. Please also note how the parameters of that route are passed.{% for ... in ... %}
is used to iterate the result set. It is also very commonly used.{% set ... %}
is used to set a local variable.- The rest is regular HTML.
Once we run the app, our render should look like something the following figure:
Conclusion
In this part, I have demonstrated how to link up the basic elements in Symfony and get your application up and running. As you can see, the amount of required PHP code was quite minimal. Also, with the help of Bootstrap, the template code was quite straightforward as well.
In the next and last part of this series, we will cover a few advanced techniques:
- Pagination
- Dynamically add tags associated with a book
- Dynamically create a water mark for the book cover
- …and more
Stay tuned, and download the source code to experiment on your own!