symfony 2_使用Symfony 2构建Web应用程序:完成

symfony 2

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

在本系列的第1部分第2部分中,我介绍了使用Symfony 2开发可正常运行的网站的基础知识。

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:

通常,当进行“图像处理”时,在最终使用<img>标签显示图像之前,我们要先讨论一个或多个应用于图像的动作。 在我的应用程序中,这分两个步骤完成:

  • 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:


        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', '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:

完整的代码位于功能coverAction中的src/tr/rsywxBundle/Controller/DefaultController.php中。 该代码非常简单明了,我将在详细视图中为您展示实际书籍封面和默认封面的输出:


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).

请注意,我已经在“ web”下创建了一个“ cover”文件夹来保存默认封面和扫描的书籍封面。 我还使用中文TTF来显示水印文本。 请随时使用您自己的字体(并将该字体复制到“ cover”文件夹中)。

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.

在流量较高的网站上,正确的做法是像Lukas White在他的文章中所做的那样,缓存自动生成的图像,但我将留给您自己尝试。

分页 (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

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;

    totalcount:告诉结果计数。 这将与rpp参数一起用于计算总页数;
  • 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;
        $half = floor($pageCount / 2);
        if ($this->page + $half > $this->totalPages) // Close to end
            while ($i >= 1)
                $r[] = $this->totalPages - $i + 1;
            return $r;
        } else
            while ($i >= 1)
                $r[] = $this->page - $i + $half + 1;
            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.

为了确保此功能确实有效,我们必须在使用前对其进行测试。 我们将使用PHPUnit作为测试平台。 请参阅官方网站以获取有关如何安装它的详细说明 。 我使用phpunit.phar方式下载了软件包并将其放置在项目的根文件夹中。

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:

为了测试我们刚刚创建的Paginator类,首先我们需要在src/tr/rsywxBundle/Tests下创建一个文件夹Utility 。 Symfony中的所有测试都应该在src/tr/rsywxBundle/Tests 。 在Utility文件夹中,创建一个名为PaginatorTest.phpPHP文件:

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);
            $this->assertEquals($pages, 11);
            $this->assertEquals($list, array(1,2,3,4,5));

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

            $paginator=new Paginator(10, 101, 10);
            $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.

testgetPageList函数中,我们基本上用我们能想到的任何参数组合实例化了要测试的对象(分页器)。 然后,我们调用该对象的某些方法,并使用断言测试结果的有效性。 在这里,我们仅使用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.

在我们创建的$paginator对象的示例$this->assertEquals($list, array(7,8,9,10,11)) ,我们知道总共应该有11页(总共101条记录,每页10条记录),而当前页的第10页将返回第7,8,9,10,11页列表,因为第10页非常接近末尾。 我们对此进行断言,如果断言失败,则函数逻辑中肯定有问题。

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


php phpunit.phar -c app/

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!)

这将从app/文件夹中读取PHPUnit的配置文件( phpunit.xml.dist由Symfony安装生成。请勿更改!)

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

注意:请删除Symfony自动生成的所有其他测试文件(例如Tests下的Controller文件夹)。 否则,您将至少看到一个错误。

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).

上面的命令将分析所有测试文件下Tests ,并确保所有的断言通过。 在上面的示例中,您将看到提示,提示类似OK, 1 test, 4 assertions 。 这意味着我们创建的所有测试均已通过,因此证明了该功能可以正常运行。 如果不是,那么代码中一定有什么问题(在实现中或在测试中)。

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).

我们的数据库有一个名为book_visit的表,我们使用timestamp作为数据类型来记录访问图书详细信息页面的时间。 我们需要对访问进行一些统计汇总,其中之一是按天获取总访问次数(我的“天”在+8小时时区中)。

In SQL, this is easy:


select count( 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.

在上面, 15*60*60可以将服务器时间调整为我的时区。

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:

但是,如果您尝试在Symmonfy中使用类似的语法(当然,将表名更改为其FQN),则会出现错误提示,告诉您date function is not supported 。 为了解决这个问题,一种方法是使用纯SQL:

$q = $em->getConnection()->prepare('select count( vc, date(from_unixtime(v.visitwhen+8*60*60)) vd from book_visit v group by vd order by vd');
    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( vc, date(from_unixtime(v.visitwhen+15*60*60)) vd from book_visit v group by vd order by vd', $rsm);


        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.

vc (访问计数)和vd (访问日期)在addScalarResult调用中都出现了两次。 第一个是将从查询中返回的列名,第二个是该列的别名。 为了避免创建更多名称的麻烦,我们只使用相同的名称。

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.

标量结果描述SQL结果集中的单个列到Doctrine结果中的标量值的映射。 标量结果通常用于聚合值,但是SQL结果集中的任何列都可以映射为标量值。

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.

这远不是Symfony的完整教程。 有很多未涵盖的内容(表单,安全性,功能测试,i18n等),可以轻松地包含另外10-12个部分。 我强烈建议您阅读Symfony提供的完整正式文档,可以在此处下载。

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.



symfony 2

  • 0
  • 0
  • 0
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
钱包余额 0