Symfony 框架实战教程——第三天:用 KnpPaginatorBundle 实现翻页

Symfony 框架实战教程——第三天:用 KnpPaginatorBundle 实现翻页

Chris Yue 50 comments
Posts

昨天我们已经创建好了新闻的首页。今天我们来实现添加新闻并且显示新闻的功能,并且学会使用 Composer 添加第三方 Bundle 来加速开发。

创建业务数据模型

新闻数据算是我们业务模型里必不可少的模型之一。根据我们之前对需求的分析,我们可以很容易想到,新闻模型News需要的属性:

  • 标题属性
  • 文本属性

接下来,我们要在 AppBundle 里创建它,但是这些数据还需要一个持久层来保存数据,例如之前配置的 Mysql。目前流行的开发方式,无论是 Java 还是 ROR,都会使用 ORM 将数据库字段和类属性关联起来。

Symfony 框架本身并不包含 ORM 工具(严格意义上来说,Symfony 框架,即 FrameworkBundle,不包含 ORM,安全组件,模板引擎,日志工具,邮件组件等一系列工具),只不过 Symfony installer 将一些推荐的,Web 开发常用的工具,都默认安装了。如果你已经知道如何用 Mysql 来存储/获取数据,也不一定非要用 ORM。这里我们为了快速开发,也为了省一些精力,就使用默认提供的 Doctrine2 ORM,它会给我们的开发带来许多便利。

决定了使用 Doctrine2,我们除了定义好 News 类,还需要写配置文件,让 Doctrine2 ORM 将 News 类同数据库某个表关联起来。听起来要做的工作不少,不过且慢,DoctrineBundle 里自带的代码生成工具能让我们的开发再快一点点:

此时会有欢迎提示出现,并且让你输入一个模型的“短名字”,为什么说是要输入短名字?News 并不是全名,全名应该是包含命名空间的,比如我们的 News 全名应该是 AppBundle\Entity\News,Entity 是什么?Entity 只是一个习惯性叫法。在 Doctrine2 的世界里,只要是 ORM 过的模型,都叫 Entity(除此之外还有用 MongoDB 作为存储方案的 ODM,ODM 过的模型习惯称之为 Document)。

后面我们就按照他们的提示,分别输入:

刷新 src/AppBundle 目录,多了一个 Entity 目录,此目录包含了两个文件:News.phpNewsRepository.php

打开 News.php 我们可以看到,News 类已经生成好了,并且还有用注解格式写的 ORM 配置。我的建议是可以用生成代码工具尽量用,一是快,二是不容易写错字。

此时,我们就可以用 DoctrineBundle 的数据库操作工具来生成数据的数据库和表了:

数据库创建好之后,我们应该创建“新建新闻”页和“新闻详情”页、以及更新我们之前写的“新闻首页”。不过且慢,DoctrineBundle 不仅仅能生成 Entity,它还能根据 Entity 直接生成相关的 Controller!这里我们先把之前的 NewsController 类文件删掉。然后使用下面命令:

如同之前的命令,我们只用回答它的问题就行了:

我们会发现,被删除的 NewsController 又被重新生成,并且多了好多代码。先不管每个控制器方法里写了啥,我们先检查路由配置。我们会发现,一切都很好,除了首页的路由名字从以前的 news_index 变成了 news,此时我们可以将 news 改回 news_index,也可以考虑将之前的 news_index 定为 news。我们这里选择后者,毕竟已经为我们定义了一套规则,并且也不赖,何必再去折腾别的命名方式?还好代码也不多,目前我们只用把首页模板 default/index.html.twig 里的 news_index 改成 news 就行了。

doctrine:genearte:crud 做的事情就比较多了,它不仅生成了控制器,所有的模板文件也都生成了,并且还生成了表单类。我们先不管表单类,先访问新闻首页 /news/ 试试,没有意外的话,你们可以看到一个从新建、显示、编辑、删除都完全可用的新闻功能。

需要注意的是:从 Symfony 2.6 开始,模板文件推荐是放在 app/Resources 下的,但是 doctrine:generate:crud 命令还是将模板文件放在了 AppBundleResources 目录。不仅如此,也不推荐使用 @Template 注解来猜模板路径(官方说法:主要因为性能问题),所以这里我们得把生成的 src/AppBundle/Resources 目录移到 app 目录,并且去掉控制器类里的所有 @Template 注解,而直接使用 $this->render 方法。

有一些我们暂时用不着的方法,可以先去掉。比如删除相关的方法 deleteAction 以及 createDeleteForm,以及相关的调用全部去掉。模板里面有用到 delete_form 的地方,也都删掉。为了节省代码,我们把 newAction/createAction,以及 editAction/updateAction 合并为一个方法,这也是 Symfony 官方所推荐的最佳实践:

注意合并之后,之前的两个路由 news_createnews_update 已经没有了,所以 NewsController 里相关的代码,都需要更新。

2016年06月13日更新:感谢网友提醒,从 2.8 版本开始,doctrine:generate:crud 已经按照最佳实践将模板生成到了 app/Resources 目录,而且不再使用 @Template 注解,以及合并了 newAction/createAction 和 editAction/updateAction 方法。

另外细心的同学会发现注解 @Method 也被我删除了,因为 @Route 其实是包含 @Method 的功能的。这里看个人爱好。

如果你已经清理干净了 @Method@Template 注解,那么以下的代码就可以删除了:

反过来也说明一个需要小心的点:如果你要使用某个注解功能,是需要 use 到当前代码文件的。

使用 KnpPaginatorBundle 实现翻页功能

现在有一个小问题,列表总是需要翻页的。此时大家如果心里在琢磨“翻页算法”,那说明你的开发方式还不够 Symfony,或者说还不够敏捷。像翻页这种常见的需求,现成的库一定是很多而且已经非常成熟,一个人花时间做设计和开发,让成千上万的其他人节省时间,正是开源软件伟大之处之一,除非是自己个人爱好,省时间的事情是一定要做的,毕竟工程师的主要职责是实现而不是钻研。目前支持 Doctrine2 的翻页库也有二三,而且大都已经 Bundle 化了。这里我推荐一款用得比较多的:KnpPaginatorBundle

安装第三方 Bundle 需要使用 Composer。安装 Composer 不在本篇范围内,大家可以上网搜搜,也可以看看我这篇 博客。这里我只用提醒大家一点:因为某种众所周知的原因,Composer 的速度不是很快,大家可以试试镜像

准备好 Composer 以后,执行以下命令(假设已经把 composer.phar 改名为 composer

昨天说过,之后将 Bundle 的“代表类”注册到 AppKernel 里,才能使用 Bundle 所提供的配置文件以及“服务”,什么是服务,稍后会说。

我们先完成 KnpPaginatorBundle 的注册:

然后新闻首页的代码做一点更新:

其中 $this->get('knp_paginator') 得到的就是一个服务。目前不用深究服务是什么,只用知道服务也是一个对象,此对像会依赖一些其他的对象。如果手工初始化,需要写很多代码(依赖的对像的初始化,依赖的对象的依赖对象的初始化……),而 Symfony 的 Service Container 组件,可以通过配置文件来描述这种对象之间的依赖关系,进一步在实际使用的时候,只需用 $this->get 方法通过一个设置好的唯一名字去获取组件就行了($this->get() 其实是 $this->container->get() 的快捷方式)。依赖注入和服务容器是 Symfony 框架中运用得很多的概念之一,目前有个感觉就行,以后还会提到。

相应的模板文件app/Resources/views/news/index.html.twig也做一点调整:

分页我们就实现了。

这里我给大家介绍一下 createQueryBuilder 方法。首先大家应该知道的是,从数据库里获取数据,需要向数据库发起数据请求,一般来说是一段字符串,比如大家最熟悉不过的 SELECT * FROM xxx 这样的 SQL 语句,这个请求就叫“Query”,而“Query Builder”就是指创建 Query 的东西。其实有了 QueryBuilder 这个东西,在拼装 SQL 语句的时候,会方便许多,也不容易出错。大家可以对比一下以下代码:

需要各位读者注意的是,如果记录条数不满每页记录数,分页控件是不会出现的(KnpPaginator 默认是 10 条,您也可以自己设置每页记录数,至于怎么调,留给大家当作业吧)

第四天

Symfony 框架实战教程——第三天:用 KnpPaginatorBundle 实现翻页 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

文章不错,我要帮站长分担建站费!
天使投赏人

50 Comments

freechoice

九月 17, 2019 在 12:00 下午

Symony 4.3里使用Knp-paginator-bundle,
Service “knp_paginator” not found: even though it exists in the app’s container, the container inside “App\Controller\ContentController” is a smaller service locator that only knows about the “doctrine”, “form.factory”, “http_kernel”, “parameter_bag”, “request_stack”, “router”, “security.authorization_checker”, “security.csrf.token_manager”, “security.token_storage”, “serializer”, “session”, “templating” and “twig” services. Try using dependency injection instead.不知道是不是换了使用方式,直接使用Pager了

    Chris Yue

    九月 19, 2019 在 9:58 上午

    很有可能是。错误信息其实有很详细的解释,可以获得以下信息:

    1. knp_paginator 的服务还是存在容器里的
    2. 但是 controller 里的容器并不是之前版本那个整个框架用的容器,而是一个更小的容器
    3. 这个小容器里并没有 knp_paginator

    另外它说了建议:

    直接通过依赖注入的方式,把 knp_paginator 注入到 controller 里(具体怎么注入我这里就不说了,又是一篇文章的量,但应该很好搜索)。

    另外多说一句,其实通过注入服务的方式是好于之前直接使用 container 的,目的是为了让控制器明确自己的依赖。

     

leo

七月 5, 2017 在 10:18 上午

楼主 为啥 添加新闻不好使了呢 ,没走 create但是直接走到index了

    Chris Yue

    七月 6, 2017 在 9:13 上午

    你确认是表单是走的 POST 方法吗?

     

YIPU

一月 3, 2017 在 1:45 下午

AppKernel::init() method is deprecated since version 2.3 and will be removed in 3.0. Move your logic to the constructor method instead.这个怎么解决啊,谢谢

    Chris Yue

    一月 4, 2017 在 12:43 下午

    如果我没记错的话,AppKernel::init() 方法应该是在比较早之前的版本里在 web/app.php 里执行的一段代码。你的 Symfony 应该升级过,但 web/app.php 还没有更新,目前 Symfony3 以上已经不在 web/app.php 里执行 AppKernel::init() 方法了。

     

好博客 坚持,可以挂点广告 我帮你点。。。

八月 16, 2016 在 2:43 下午

好博客 坚持,可以挂点广告 我帮你点。。。

    Chris Yue

    八月 17, 2016 在 10:44 上午

    噗~谢啦兄弟

     

匿名

六月 13, 2016 在 10:38 上午

在Symfony 2.8.7版本中,doctrine:generate:crud 命令已经将模板文件放在了 app/Resources 下的目录。也不再使用 @Template 注解来猜模板路径,而直接使用 $this->render 方法。

而且 newAction/createAction,以及 editAction/updateAction 也已经合并为一个方法,这也是 Symfony 官方所推荐的最佳实践

 

    Chris Yue

    六月 13, 2016 在 2:06 下午

    感谢提醒

     

东东包

三月 23, 2016 在 3:35 下午

使用knppaginatorbundle可以实现ajax分页吗?

    Chris Yue

    三月 24, 2016 在 9:11 上午

    可以啊,knppaginatorbundle可以只提供数据

     

万仕红

二月 2, 2016 在 10:54 上午

用 sonata-project/admin-bundle 做的后台,上线2个月左右,出现了一个性能问题,把调试工具打开发现在查询列表的时候 有2条SQL语句执行特别慢

以上两条SQL随着项目的增大,执行时间越来越长。目前150W左右数据执行时间需要 18058.10 ms
我猜想是 knplabs/knp-paginator-bundle 在分页和查询总数的时候 总数带上了关键字 “DISTINCT”

我把这两条语句的关键字 DISTINCT 去掉以后,直接拿到数据库执行只需要 5ms 。所以我去查找 symfony2、sonata-project/admin-bundle、knplabs/knp-paginator-bundle 对于这个关键字的处理或者配置,但是持续查找了快一个礼拜都没有结果。

在此希望博主如果有时间的话,能够帮我分析分析。

感激不尽

    Chris Yue

    二月 3, 2016 在 1:24 下午

    我刚才去KnpPaginator的项目站看了一下,有一个配置是控制count时是否用DISTINCT的:

    但这个选项也只能控制count那一句,select那一句是控制不了的。

    之所以有这个选项,我想应该是作者为了和Doctrine2的做法一致吧,select distinct是Doctrine2加上的

    这个问题以前我也有注意过,但一直不明白为什么Doctrine2非要多写一句select distinct,如果你知道原因也麻烦跟我说下,谢谢,我非常好奇……

     

xiaoyu

十一月 27, 2015 在 2:57 下午

KnpPaginatorBundle  页面怎么加载样式呢!

    Chris Yue

    十一月 30, 2015 在 12:33 上午

    config.yml里:

    例子里的twig文件是默认值,你可以参考此文件自己写一个模板,然后再配置成自己的twig文件路径

     

蚊子

十月 18, 2015 在 4:27 下午

还有就是$paginator实例是怎么来的,我写$pagination = $paginator->paginate 的时候没有代码提示,我要new pagination吗?

    Chris Yue

    十月 25, 2015 在 10:30 上午

    代码提示是IDE提供的辅助功能,但因IDE的不同,代码提示的能力也不一样。如果只是IDE没有出现提示,不一定代码有问题,以实际可运行为主。

     

蚊子

十月 18, 2015 在 4:01 下午

请教一下作者,看了你的教程受益匪浅,只是有个问题不清楚,还没去写代码调试。

$paginator = $this->get(‘knp_paginator’);   里的这个knp_paginator,没看到你在哪里声明这个id呀 ,这样可以这个id的服务吗?

    Chris Yue

    十月 25, 2015 在 10:27 上午

    knp_paginator服务是KnpPaginatorBundle里面定义的。Bundle是可以定义服务的

     

wimi

九月 22, 2015 在 9:22 上午

Case mismatch between loaded and declared class names: AppBundle\Entity\news vs AppBundle\Entity\News
这是怎么回事?

    Chris Yue

    九月 24, 2015 在 9:04 下午

    抱歉你的评论自己跑到垃圾评论里了。

    你检查一下你的Entity\News的文件名是大写开头的News.php么?这个文件应该是Windows下的吧我还从来没遇到过

     

freechoice

九月 19, 2015 在 10:43 上午

翻页是可以用了,如果更换了其他的Subscriber,比如KnppaginatorBundle首页上的那个分类文件的翻页,还写了个Service,这时候如果还想从数据库取记录出来,就不知道怎么换回来了,还请大哥指教啊

    Chris Yue

    九月 21, 2015 在 1:47 下午

    额……你说啥……

    你是想不用分页控件直接从数据库里取数据?

     

    freechoice

    九月 21, 2015 在 9:30 下午

    不是不想用分页控件从数据库当中取数据,而是同时从数据库和其他数据源取数据,默认情况下是用Doctrine的QueryBuilder取数据么,在KnpPaginationBundle的官方的Subscriber.md当中有介绍使用其他的数据源,比如说读取某个文件夹当中的文件列表进行分页(官网就是这个例子),我自己也写了一个从其他地方取数据的Subscriber,工作的挺好,至少摆脱了数据库的束缚了,摆脱了这个束缚,新的问题来了,当我想用数据库的时候,我没办法切换到数据库中的记录分页,我知道Knppaginationbundle有多个Subscriber,但我不知道怎么切换他们。 http:/www.aiduv.com,使用的就是其他的数据源,不是数据库,但我想用数据库的记录分页的时候,傻眼了

     

    freechoice

    九月 21, 2015 在 11:57 下午

    官网的文档在这里:https://github.com/KnpLabs/KnpPaginatorBundle/blob/master/Resources/doc/custom_pagination_subscribers.md
    我提交的问题在这里:https://github.com/KnpLabs/KnpBundles/issues/465

     

    Chris Yue

    九月 24, 2015 在 8:57 下午

    OK我知道你说的什么意思了。我大概瞄了一下KnpPaginatorBundle的代码,最简单粗暴的方式就是不要使用symfony的service tag功能,你自己根据使用情况手动调用event_dispatcher服务的addSubscriber方法,比如直接在某些不需要Doctrine的Controller使用自己的Subscriber,在某些需要Doctrine的Controller用默认的Subscriber。

    只不过Subscriber不是那么用的,要正儿八经写我建议还是通过target来判断——人家例子其实写得很详细了——如果target是一个需要你自己分页的对象,那就处理一下分页方式。如果不是,比如target是个Doctrine QueryBuilder对象你就直接return就行了

     

ko1079

九月 1, 2015 在 5:34 下午

遇到一个特别诡异的问题,纠结了我一下午 楼主看看
FatalErrorException: Error: Call to undefined method Doctrine\ORM\Query::hasHint() in E:\www\symfony\Symfony\vendor\knplabs\knp-components\src\Knp\Component\Pager\Event\Subscriber\Paginate\Doctrine\ORM\QuerySubscriber\UsesPaginator.php line 37

    Chris Yue

    九月 2, 2015 在 10:32 上午

    我从来没有遇到过这个问题,不过看起来像是doctrine版本的问题,你可以尝试运行`composer update`命令来更新所有第三方库,如果还不行的话,你可能得给doctrine降级安装

     

txsing

五月 22, 2015 在 11:02 上午

博主,我的代码跟你的一模一样,设置了每页1条新闻,可是结果是 第一页上依然显示所有的新闻,尽管下面的导航条显示了有4页,这是什么情况的

    Chris Yue

    五月 25, 2015 在 4:34 下午

    你这属于疑难杂症了……我还真没看出来哪儿不对的,等会儿……不应该是$paginator->paginate($qb, $pageNumber, $pageSize)吗,你试试

     

leo

五月 14, 2015 在 3:26 下午

完全是新手,这一天可真不容易啊
一上午反复折腾到现在,在@Template 换 $this->render这里折腾了好久,中途想放弃换其他教程的,但又不甘心浪费的前两天学习的时间.
如果博主的文章能更详细点就好了。

感谢!

    Chris Yue

    五月 15, 2015 在 1:10 下午

    没问题。你在替换@Template的过程中遇到什么问题了?有什么细节容易忽略的,能说一下吗?我补充一下

     

    leo

    五月 15, 2015 在 3:56 下午

    原来@Template是写在public function 上面的注释中的,替换的话需要使用$this->render() 需要把函数中return的数组代入并把注释中的@Template()删除掉,开始我不知道在这卡了好久,除了这里这一篇我还挺顺利的。

     

blooe

四月 30, 2015 在 2:55 上午

你好,查了半天还是不太理解createQueryBuilder(‘n’)的意思,可以解释下吗?

    Chris Yue

    四月 30, 2015 在 6:45 下午

    为了让其他更多人能看到,我还是写在文章里面吧,你可以在本页搜索一下”createQueryBuilder”定位

     

blooe

四月 29, 2015 在 5:56 下午

应该给读者提醒一下,新手容易被误导。一开始因为我的总条数没达到它默认的每页新闻条数,一直没显示页码,搞得我以为哪里出错了,后来设置了paginate第三个参数,才出现了页码

    Chris Yue

    四月 30, 2015 在 6:41 下午

    同感,立马加上

     

Alex Zheng

四月 16, 2015 在 4:30 下午

问你,你这里创建了数据库,按照symfony在app/config/parameters.yml.dist这个文件中的配置,应该在mysql中看到一个名字叫symfony的数据库才对。
但是在我这里没有看到,不知道是什么原因。

还请指教

    Chris Yue

    四月 24, 2015 在 9:10 上午

    真正的配置文件不是parameter.yml.dist而是parameter.yml 不好意思你的评论被当成垃圾评论了现在我才看到……

     

蔡司基

四月 2, 2015 在 6:52 下午

flush后我怎获取影响条数?

    Chris Yue

    四月 2, 2015 在 7:45 下午

    问一遍就行了……你多余的评论我先删了啊。

    如果你是非要获取数据库返回的影响条数,你可以通过nativeQuery来执行Mysql的——如果我没记错的话—— ROW_COUNT() 语句来返回影响条数。

    但这样并不好,因为ROW_COUNT()不一定其他数据库会提供一样的函数,万一换库了你就悲剧了,另外也因为多执行了一条SQL语句。

    还有一个方法就是不用Doctrine ORM而只用Doctrine DBAL去执行update语句,文档中的$conn就是 $this->getDoctrine()->getManger()->getConnection()

     

ccna30

三月 31, 2015 在 9:05 下午

我在用pagination的时候出现一个常见问题,就是想把数据表里面的图片路径,比如1.jpg 填入模板的<img标签:如

这样当然可以出现图片,但是其中的public/1.jpg用{{ article.face }}替换之后,输出还是src=”/web/{{ article.face }}”,感觉是嵌套的{{不起作用。
如果想在控制其中将article.face传过来,可是却不知道如何从$pagination对象取出face,感觉是传过来是整个的查询result,能否取出特定字段你?
pagination的文档上也没有相关内容。

    ccna30

    三月 31, 2015 在 9:23 下午

    当然
    <img src=”/lq/web/public/{{ article.face }}”..
    将路径写死就能解决这个问题,这样就不能用asset了。

     

    Chris Yue

    三月 31, 2015 在 10:50 下午

    {{ }}不能嵌套用的亲……跟<?php ?>里不能再套一个<?php ?>一个意思……

    这样写就行了吧

     

ccna30

三月 27, 2015 在 12:13 下午

安装了KnpPaginatorBundle之后,也修改了模板,但是结果是白屏。

    ccna30

    三月 27, 2015 在 12:14 下午

    模板代码为

     

    ccna30

    三月 27, 2015 在 12:23 下午

    访问http://localhost/news/web/app_dev.php/news/,就是白屏,无任何错误信息,查看源代码为空。。

     

    ccna30

    三月 27, 2015 在 1:56 下午

    这节我弄了三天了,还是白屏,于是我修改控制器最后一句为:
    return new Response(“OK”);
    得到了OK的输出,说明是模板渲染没成功。

     

    Chris Yue

    三月 27, 2015 在 9:40 下午

    你还真是够拼的。我看了一下你的代码,首先你得把注释里的@Template那行给去了,然后确认模板文件的位置是在项目根目录下app/Resources/views/news/index.html.twig

     

发表评论

36 − 32 =