Building a Web App With Symfony 2: Development

Taylor Ren
This entry is part 2 of 3 in the series Building a Personal Web App Head To Toe With Symfony 2

Building a Personal Web App Head To Toe With Symfony 2

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:

  1. Look at how the parameters we defined in route book_list (page and key) 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.

  2. Look at how the POSTed parameters in a form are passed into searchAction and how we retrieve the necessary information from the submitted data.

  3. getParameter is a function to retrieve global parameters. The global parameters are defined in the file app/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!

Building a Personal Web App Head To Toe With Symfony 2

<< Building a Web App with Symfony 2: BootstrappingBuilding a Web App with Symfony 2: Finalizing >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Anonymous

    If you use mainly Symfony on your host and you can install PHP exntensions on it, this one might be useful:
    https://github.com/derickr/Twig

    • Taylor Ren

      I am using Twig…