Building a Web App with Symfony 2: Finalizing

Share this article

In Part 1 and Part 2 of this series, I have covered the basics of using Symfony 2 to develop a functioning web site.

In this part of the tutorial, I will cover some more advanced techniques and finish the project with pagination, image watermarks and NativeQuery.

The code we’ll be using is identical to the code from Part 2 – the features are already there, they just weren’t discussed.

Image watermarks

Seeing as there’s already plenty of image manipulation and watermarking through Imagick tutorials available on SitePoint, and owing to the fact that we don’t do any advanced image manipulation in this particular project, we’ll stick to PHP’s default image mainpulation library – GD.

In general, when there is “image processing”, we are talking about an action or actions applied to an image before we eventually show it using the <img> tag. In my application, this is done in two steps:

  • Create a route to capture the “image display request” and map it to an action in a controller.
  • Implement the processing in the action.

Route configuration

The route for displaying an image after processing is simple:

    cover: 
        pattern: /books/cover/{id}_{title}_{author}_{width}.png
        defaults: {_controller: trrsywxBundle:Default:cover, width: 300}

In a template, we can invoke the call to display the processed image by:

    <img src="{{path("cover", {'id':book.id, 'author':author, 'title':book.title}) }}" alt="{{book.title}}'s cover" title="{{book.title}}'s cover"/>

So every time this <img> tag is encountered, a processed image will be displayed.

Image processing

In this application, we do two things before displaying the image:

  1. Depending on whether or not a book cover image exists, display it, or display the default cover image;
  2. Add a watermark and adjust the size (300px wide in book detail view and 200px wide in reading list view).

The complete code is in src/tr/rsywxBundle/Controller/DefaultController.php in the function coverAction. The code is simple and straightforward and I’ll just show you the output in Detail View for both a real book cover and a default cover:

Note that I have created a “cover” folder under “web” to hold the default cover and the scanned book covers. I also used a Chinese TTF to display the watermark texts. Please feel free to use your own font (and copy that font to the “cover” folder).

On a higher-traffic site, the correct course of action would be to cache the autogenerated images much like Lukas White did in his article, but I’ll leave that up to you to play around with.

Pagination

There are also a lot of articles on paginating a big dataset. In this tutorial, I will show you how I did it in this app, and we’ll test it.

The source code for the class is in src/tr/rsywxBundle/Utility/Paginator.php.

The code itself is easy to read and does not involve anything particularly advanced, so I will just discuss the process.

There are two fundamental values in a Pagination class:

  1. How many records in total and how many pages in total?
  2. What is the current page and how to construct an easily accessible page list for further processing ?

Many pagination classes often deal with data retrieval too, but this is not good practice: pagination should only deal with things related to pagination, not the data itself. The data is the domain of the Entity/Repository.

If we go back to the implementation of getting the books matching certain criteria, you will notice how the two steps (get data and do pagination) are separated in the controller:

File location: src/tr/rsywxBundle/Controller/BookController.php

    public function listAction($page, $key)
    {
        $em = $this->getDoctrine()->getManager();
        $rpp = $this->container->getParameter('books_per_page');

        $repo = $em->getRepository('trrsywxBundle:BookBook');

        list($res, $totalcount) = $repo->getResultAndCount($page, $rpp, $key);
        //Above to retrieve data and counts
        //Below to instantiate the paginator

        $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));
    }

The constructor of my paginator takes 3 parameters:

  • page: to tell what is the current page. This will be used to return a list for pages to show as clickable links in the template;
  • totalcount: to tell the count of the results. This will be used to calculate the total pages together with the rpp parameter;
  • rpp: short for records per page.

In my current implementation, I used a simple version of pagination showing only “First”, “Previous”, “Next”, and “Last” page links, but you can try out different types by using the getPagesList function.

Take for example a page list like this:

    1 2 3 4 5

The key here is in the getPagesList function which makes sure that the current page is always in the middle, or if there aren’t enough pages, it makes sure it’s in the correct position.

    public function getPagesList()
    {
        $pageCount = 5;
        if ($this->totalPages <= $pageCount) //Less than total 5 pages
            return array(1, 2, 3, 4, 5);

        if($this->page <=3)
            return array(1,2,3,4,5);

        $i = $pageCount;
        $r=array();
        $half = floor($pageCount / 2);
        if ($this->page + $half > $this->totalPages) // Close to end
        {
            while ($i >= 1)
            {
                $r[] = $this->totalPages - $i + 1;
                $i--;
            }
            return $r;
        } else
        {
            while ($i >= 1)
            {
                $r[] = $this->page - $i + $half + 1;
                $i--;
            }
            return $r;
        }
    }

To make sure this function really works, we’ll have to test it before using it. We’ll use PHPUnit as the test bench. Please refer to the official site for detailed instructions on how to install it. I used the phpunit.phar way to download the package and place it in my project root folder.

To test the Paginator class we just created, firstly we need to create a folder Utility under src/tr/rsywxBundle/Tests. All tests in Symfony should go under src/tr/rsywxBundle/Tests. In the Utility folder, create a PHP file named PaginatorTest.php:

    namespace tr\rsywxBundle\Tests\Utility;

    use tr\rsywxBundle\Utility\Paginator;

    class PaginatorTest extends \PHPUnit_Framework_TestCase
    {
        public function testgetPageList()
        {
            $paginator=new Paginator(2, 101, 10);
            $pages=$paginator->getTotalPages();
            $this->assertEquals($pages, 11);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(1,2,3,4,5));

            $paginator=new Paginator(7, 101, 10);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(5,6,7,8,9));

            $paginator=new Paginator(10, 101, 10);
            $list=$paginator->getPagesList();
            $this->assertEquals($list, array(7,8,9,10,11));
        }
    }

This kind of test is called a Unit Test. It tests a particular unit of the program.

In testgetPageList function, we basically instantiate the object we want to test (a paginator) with virtually any combination of parameters we can think of. We then call some methods of that object and test the validity of the result by using assertions. Here we only use the method assertEquals.

In the example $this->assertEquals($list, array(7,8,9,10,11)) from the $paginator object we created, we know there should be a total of 11 pages (with 101 records in total and 10 records per page), and page 10 as the current page will return a page list 7,8,9,10,11 as page 10 is very close to the end. We assert this and if that assertion fails, there must be something wrong in the function logic.

In our command line/terminal, we run the following command:

php phpunit.phar -c app/

This reads the configuration file for PHPUnit from the app/ folder (phpunit.xml.dist is generated by the Symfony installation. DON’T CHANGE IT!)

Note: Please delete all other test files auto-generated by Symfony (like Controller folder under Tests). Otherwise, you will see at least one error.

The above command will parse all test files under Tests and make sure all assertions pass. In the above example, you will see a prompt saying something like OK, 1 test, 4 assertions. This means all the tests we created have passed and thus proved the function behaves properly. If not, there must be something wrong in the code (in the implementation or in the test).

Feel free to expand the test file for the Paginator class.

It is always a good practice to test a home-made module before it is used in your program.

For a more in-depth look at PHPUnit and testing in PHP, see any of SitePoint’s numerous PHPUnit articles.

NativeQuery

Our database has a table called book_visit, we use timestamp as the data type to log the time of a visit to the book detail page. We need to do some statistics aggregation on the visits and one of them is to get the total visit count by day (my “day” is in the +8 hours timezone).

In SQL, this is easy:

    select count(v.bid) vc, date(from_unixtime(v.visitwhen+15*60*60)) vd from book_visit v group by vd order by vd

In the above, 15*60*60 is there to adjust my server time to my timezone.

However, if you try to use similar grammar in Symonfy (changing the table name to its FQN, of course), an error prompt will tell you something like date function is not supported. To solve this, one way is to use pure SQL:

    $q = $em->getConnection()->prepare('select count(v.bid) vc, date(from_unixtime(v.visitwhen+8*60*60)) vd from book_visit v group by vd order by vd');
    $q->execute();
    return $q->fetchAll();

Or as recommended by Symfony and Doctrine, we can (and should) use createNativeQuery and ResultSetMapping.

    public function getVisitCountByDay()
    {
        $em = $this->getEntityManager();

        $rsm=new \Doctrine\ORM\Query\ResultSetMapping;

        $rsm->addScalarResult('vc', 'vc');
        $rsm->addScalarResult('vd', 'vd');

        $q=$em->createNativeQuery('select count(v.bid) vc, date(from_unixtime(v.visitwhen+15*60*60)) vd from book_visit v group by vd order by vd', $rsm);

        $res=$q->getResult();

        return $res;

    }

In the example above, the most critical statements are to create a ResultSetMapping and add results to that mapping.

vc (visit count) and vd (visit date) both appeared twice in the addScalarResult call. The first is a column name that will be returned from the query and the second is an alias for that column. To prevent the complication of creating more names, we just use the same names.

A scalar result describes the mapping of a single column in an SQL result set to a scalar value in the Doctrine result. Scalar results are typically used for aggregate values but any column in the SQL result set can be mapped as a scalar value.

The above functionality is not implemented in the final code. Take it as home work.

Conclusion

This is far from a complete tutorial for Symfony. There’s plenty not covered (forms, security, functional testing, i18n, etc), which could easily take another 10-12 parts. I highly recommend you read the full official documentation provided by Symfony, which can be downloaded here.

This being my first time writing a series in PHP and for Sitepoint, I would appreciate any constructive criticism and general feedback you could throw my way.

Frequently Asked Questions about Building a Web App with Symfony 2

How do I implement pagination in Symfony 2?

Pagination is a crucial feature for any web application that deals with a large amount of data. In Symfony 2, you can use the KnpPaginatorBundle to implement pagination. First, install the bundle using composer and enable it in your AppKernel.php file. Then, in your controller, you can paginate a query using the ‘paginate’ method provided by the bundle. You can customize the number of items per page and the current page number. Finally, in your view, you can render the pagination links using the ‘knp_pagination_render’ function.

How do I use the addScalarResult method in Symfony 2?

The addScalarResult method is part of the Doctrine ORM Query ResultSetMapping class. It is used to map a SQL result set to a scalar value. The method takes two parameters: the column name in the result set and the name you want to use in your application. This can be useful when you want to execute a native SQL query and map the results to an entity or a scalar value.

How do I create a pager in Symfony 2?

Creating a pager in Symfony 2 can be achieved using the Pagerfanta library. First, install the library using composer and enable it in your AppKernel.php file. Then, in your controller, create a new Pagerfanta instance and set the adapter. The adapter is responsible for fetching the data and it can be a Doctrine ORM query, a Propel query, or any other type of data source. Finally, in your view, you can render the pager using the ‘pagerfanta’ function.

How do I use the ResultSetMapping class in Symfony 2?

The ResultSetMapping class is part of the Doctrine ORM Query. It is used to map the results of a native SQL query to entities or scalar values. You can add entities to the result set mapping using the ‘addEntityResult’ method and scalar values using the ‘addScalarResult’ method. Then, you can execute the query using the ‘execute’ method of the Query class.

How do I create a custom repository in Symfony 2?

A custom repository in Symfony 2 allows you to encapsulate complex SQL queries. To create a custom repository, first, create a new class that extends the Doctrine\ORM\EntityRepository class. Then, define your custom methods in this class. Finally, in your entity, specify the custom repository class using the ‘repositoryClass’ attribute in the ‘Entity’ annotation.

How do I handle form submissions in Symfony 2?

Handling form submissions in Symfony 2 involves creating a form type, rendering the form in a view, and handling the form submission in a controller. The form data is automatically bound to an entity and validated against constraints defined in the entity or the form type. If the form is valid, you can persist the entity to the database.

How do I use Doctrine migrations in Symfony 2?

Doctrine migrations allow you to version your database schema and apply schema changes in a controlled way. To use migrations, first, install the DoctrineMigrationsBundle using composer and enable it in your AppKernel.php file. Then, you can generate a new migration using the ‘doctrine:migrations:generate’ command. This will create a new migration class where you can define your schema changes. Finally, you can apply the migration using the ‘doctrine:migrations:migrate’ command.

How do I use the Symfony console?

The Symfony console is a command-line interface that provides several commands for common tasks such as generating code, running tests, and managing the database. You can list all available commands using the ‘list’ command. Each command has a help screen that you can display using the ‘help’ command followed by the command name.

How do I create a service in Symfony 2?

A service in Symfony 2 is a PHP object that performs a specific task. To create a service, first, create a new class that defines the service. Then, register the service in the service container by adding a new entry in the services.yml file. You can inject other services into your service using dependency injection.

How do I use the Symfony profiler?

The Symfony profiler is a powerful tool for debugging and optimizing your application. It provides detailed information about each request such as the execution time, the memory usage, and the database queries. You can access the profiler by appending ‘/_profiler’ to your application URL.

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.

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