人人都能看懂的全栈开发教程——数据校验

人人都能看懂的全栈开发教程——数据校验

Chris Yue No Comment
Posts

在老项目里,我们已经接触过数据校验了,还记得我们通过前端和后端都检查过任务的内容是否为空吗?但当时的检查还是过于简单,对于用户的字符类型输入,总是要明确能输入的字符的长度范围,虽然这属于产品经验,但如果产品忽略了这些检查,还是应该提醒他们加上。

因为我们数据库的任务内容长度已经限制了 255 个字符了,所以我们的最大长度也就定为最长 255 个字符。

记得在之前的老项目里,我们用 PHP 做了很简单的检查:

这个代码实际有两个问题,我们来先说 die 的问题。在 PHP 中,die (或者 exit)函数,会停止执行后面的一切代码,在框架代码中,强烈建议不要这么做,目前几乎所有的框架,都可以大致分成『初始化框架』,『运行业务代码』,『收尾工作』三个步骤,我们自己写得代码都是集中在运行业务代码阶段,而收尾工作可能还有一些比较重要的事情要做,比如搜集或保存日志等,甚至我们会通过『事件』机制,针对收尾阶段做一些扩展的功能,但如果你负责的代码使用了 die,那可能别人的业务都能享受到这些扩展的功能,就你这不块代码不行,如果不对用法加以限制,die 可能会出现在项目任何一个地方,真因为它除了问题,还很难找原因,所以除非是临时代码,在框架里最好不要用 die 或者 exit

第二个问题是,用户可能输入的,就是空格,实际上也是什么都没有输入。一般来说对于字符串类型的输入,都是会先做空白删除处理的,PHP 里也很简单,使用 trim 函数即可做到。

不过在我们引入了 Symfony 框架之后,我们还有更方便的选择。下面我们来看看如何做。

很明显,用户输入检查是一个业务问题,所以相关代码,我们应当放在 domain 里。我们先创建 domain/TaskManager.php 文件,并且输入下面的代码:

TaskManager 依赖两个接口,其中一个是 TaskRepositoryInterface,表示用来存储任务数据的『任务仓库』,另外一个是 Symfony 提供的 Symfony\Component\Validator\Validator\ValidatorInterface,一个专门用来检查类属性的『校验工具』。

ValidatorInterface 的用法也很简单,只用将要校验的对象作为 validate 方法的参数即可,如果对象数据不满足校验的要求,会返回一个 Symfony\Component\Validator\ConstraintViolationListInterface 对象,这个对象表示校验出来的错误集合,我们可以通过 count 函数来返回有多少个错误。另外,为了让显示页面能拿到错误,做信息展示,我们创建了 ValidationException 异常类:

知道了用什么校验,接下来了解如何写校验的规则。Symfony 的 Validator 工具也是可以用 XML,YAML,以及 Annotation 来配置校验规则。ORM 那章有聊过使用 Annotation 来写配置的好处,实际上 Validator 是否使用 Annotation 的道理也一样。因为我们的校验规则本身就是业务规则,所以跟同样属于业务数据的 Domain\Entity\Task 类放一起不但没有问题,而且因为在一个文件里,如果要增加或者删除什么字段,改起来反而还更方便了。我们将对任务内容的检查规则,添加到 Task 类:

我觉得配置应该看起来挺直白的,就不用再解释了。读者如果有兴趣,可以通过官方文档了解 Symfony Validator 提供的所有检查种类以及用法。

业务代码写完了,记得将单元测试补充上:

当我们执行所有的测试时,会发现 UserManagerTest 现在也有报错,不过很容易看出是因为我们之前修改了 UserRepositoryInterface 接口之后导致的,因为跟本篇主题无关所以我就不演示代码了,大家可以自行尝试修复,改不好也没关系,文章最后提到的项目里的代码也可以参考。从这里我们也能窥见使用单元测试的好处,虽然我们还没有代码真正用上 UserManager::login 方法,但是我们已经通过测试知道此方法需要更新了。

另外关于单元测试这里再多提一点,大家可能已经发现,到目前为止,只针对业务代码做了测试,这里要解释一下,并不是说非业务代码就不能做单元测试,实际上所有的类都可以做单元测试,例子里没有写一是因为我觉得不用举那么多例子,二是在精力有限的情况,我们其实可以去选择测试相对更核心的功能(比如业务代码,毕竟这个项目选择了以业务为核心)。

一切没问题,我们就可以在控制器里使用 TaskManager 类了。更新 src/Controller/TaskController.php 代码如下:

将我们的 templates/task/new.html.twig 模板也改成使用 Symfony Form 的方式:

另外,为了让错误信息的显示更美观,我们还加了一个 CSS class .error,在 public/main.css 里我们来定义这个 class:

为了方便,我们依然在首页模板 templates/task/index.html.twig 添加新增任务页的链接,但这次写法有点不同:

到这里大家应该接触两次『路由』的概念了,我不打算对路由功能做详细的介绍,但读者也不用担心,大家依然能在后面的章节里慢慢体会路由的更多用法。这里只提一下使用路由的目的:像之前的老项目,所有的访问地址都写死在模板里,如果地址要做调整,那么所有用到被调整的地址的模板代码都需要改,不但繁琐还容易漏,但如果我们使用路由系统,用一个路由名去指代地址,改地址就会变得很简单,模板不用再改任何地方,只要路由名对应的地址变了所有模板的地址都会自动更新。

还记得控制器里的路径配置的写法吗?实际上现在所有配置都忽略了路由名参数 name,如果不指定的话,默认就是 app_控制器名_方法名。如果嫌默认路由名太长,也可以指定名字,比如首页:

如果项目的路径太多记不住了,可以通过下面的命令列出所有的路由名:

另外,当前访问的页面的路由名,就显示在 Symfony 最下方的调试工具条的左二位置。

说了太多路由,我们继续测试数据校验功能。点击新增按钮,这个时候发现 CSS 样式并没有加载。原因也很简单,我们一开始在 base.html.twig 里引用 main.css 的时候,路径写的是『相对路径』,什么是相对路径呢?现在新增任务的页面地址是 /task/new,那么 main.css 这个路径,就是相对于父目录地址来说的,也就是 /task/,所以最后浏览器识别的路径是 /task/main.css,这个地址当然是不对的。解决办法也很简单,使用『绝对路径』就可以直接忽略父目录,直接从根目录开始,绝对目录用法也很简单,以 / 开头就行,即把 CSS 文件路径改成 /main.css

修复完我们之前遗留的问题,我们终于可以看到正常的新增任务页面了。我们现在做两个测试,第一个测试是,我们只输入空格,会发现报错,虽然我们并没有做字符串空格处理,但 Symfony Form 会自动将用户提交内容做 trim 操作。第二个测试,我们故意输入超过 255 个字节的内容(为了好测试,我们也可以临时将校验规则里的 255 改成一个比较小的数字),也会发现错误的提醒,并且错误提示就是我们配置的内容。

文章快到结尾了,我觉得有个要点还是需要请读者注意的。实际上,如果一开始选择直接在 Symfony 这个框架上搭建项目,我们的代码还可以写得更简单。现在看 Symfony Form 和 Symfony Validation 两个组件好像没啥关联,但它们默认就是组合起来使用的,按 Symfony 默认使用方法,代码还会更少一点点,而且还能用到我们熟悉的 make 命令来自动创建表单(用 bin/console make:form 命令,有兴趣可以试试),但问题是,我们已经采纳了业务驱动的开发方式,这种方式要求 Symfony Validator 需要『粘』在我们的业务代码里,而不是 Symfony Form 里。为了坚持 DDD 的做法,我们不得不放弃工具本身提供的便利。可能在座的读者,有的以前也看过一些开发教程,它们可能都能很完美的从自己的某个角度解决某一类问题,但真实世界的项目不会像书上说得那么简单,真实的项目你可以从很多角度去看待它,不同的角度所产生的解决方式之间往往会有冲突,需要取舍。面对我们现在遇到的具体问题,我的建议是,开发方式角度优于工具角度,理由也很简单,按工具角度作为开发方向,那很容易被工具牵着鼻子走,再说,一个项目可能用到不只一种工具,万一某几个工具本身开始打架了,那不又得纠结半天。

可能有些 Symfony 新手读者并不太能感受到我说的 Symfony Form 结合 Symfony Validation 一起使用的问题,如果是这样建议读一读官方相关教程。假如大家对直接在 Symfony 框架上搭建项目感兴趣,我也会更新我以前的教程到 Symfony 5,或者重新写一个新的。

本章节完整代码见这里

人人都能看懂的全栈开发教程——数据校验 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

如果觉得文章还不错,就请扫码鼓励一下作者吧
天使打赏人

发表评论

78 + = 88