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.

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

添加水印并调整大小(书本详细信息视图中为300px宽，阅读列表视图中为200px宽)。

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

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.

rpp：每页记录的缩写。

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. 为了确保此功能确实有效，我们必须在使用前对其进行测试。 我们将使用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);
$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.

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

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.

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.

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.

symfony 2

• 0
点赞
• 0
评论
• 0
收藏
• 扫一扫，分享海报