Building a Web App With Symfony 2: Development

Share this article

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!

Frequently Asked Questions about Building a Web App with Symfony 2

How do I upgrade from Symfony 2 to a major version?

Upgrading from Symfony 2 to a major version involves several steps. First, you need to update your composer.json file to require the new version of Symfony. Then, you need to update your dependencies with the command ‘composer update’. After that, you should check for deprecated features that you might be using in your code. Symfony provides a tool called the Symfony Upgrade Fixer which can help you with this. Finally, you need to test your application thoroughly to ensure that everything works as expected.

What are some common issues I might encounter when upgrading from Symfony 2 to a major version?

Some common issues you might encounter when upgrading from Symfony 2 to a major version include deprecated features, changes in the way certain functions work, and compatibility issues with other packages or bundles you might be using. It’s important to thoroughly test your application after upgrading to ensure that everything works as expected.

How can I optimize my Symfony 2 application for better performance?

There are several ways to optimize your Symfony 2 application for better performance. These include using the built-in caching system, optimizing your database queries, and using the PHP OPcache extension. Additionally, you can use tools like Blackfire.io to profile your application and identify potential performance bottlenecks.

How do I handle database migrations in Symfony 2?

Symfony 2 uses a library called Doctrine for database interactions, which includes a powerful migrations system. You can generate migration classes using the command ‘php app/console doctrine:migrations:generate’, and then fill in the up() and down() methods with the SQL queries you want to run. Once your migration class is ready, you can execute the migration with the command ‘php app/console doctrine:migrations:migrate’.

How do I handle form validation in Symfony 2?

Symfony 2 provides a powerful form validation system that allows you to define validation rules for your form fields. These rules can be defined in your form type classes, or in your entity classes using annotations, YAML, or XML. Once your validation rules are defined, you can check if a form is valid using the isValid() method, and get any validation errors using the getErrors() method.

How do I handle user authentication and authorization in Symfony 2?

Symfony 2 provides a powerful security component that allows you to handle user authentication and authorization. You can define your own authentication providers, or use one of the built-in providers like form login or HTTP basic authentication. For authorization, you can use the built-in access control system, or define your own voters for more complex authorization rules.

How do I handle file uploads in Symfony 2?

Symfony 2 provides a file upload feature through its form component. You can create a form field of type ‘file’, and then handle the uploaded file in your controller. Symfony will automatically move the uploaded file to a temporary location, and you can then move it to its final location using the move() method of the UploadedFile class.

How do I handle errors and exceptions in Symfony 2?

Symfony 2 provides a powerful error handling system. You can create custom error pages for different HTTP status codes, and handle exceptions in your controllers using the @Exception annotation. Additionally, you can use the Monolog library, which is integrated with Symfony, to log errors and exceptions.

How do I create and use services in Symfony 2?

Services in Symfony 2 are objects that perform global tasks. You can define services in your services.yml file, and then retrieve them in your controllers using the get() method of the container. Services can also have dependencies, which are injected into the service by the container.

How do I use the command line interface in Symfony 2?

Symfony 2 provides a powerful command line interface called the Console Component. You can use it to run tasks like clearing the cache, running database migrations, and generating boilerplate code. You can also create your own custom commands by creating command classes and registering them with the console.

Taylor RenTaylor Ren
View Author

Taylor is a freelance web and desktop application developer living in Suzhou in Eastern China. Started from Borland development tools series (C++Builder, Delphi), published a book on InterBase, certified as Borland Expert in 2003, he shifted to web development with typical LAMP configuration. Later he started working with jQuery, Symfony, Bootstrap, Dart, etc.

oopPHPsymfonysymfony2tutorialweb app
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week