这是学习PHP的最后一天,我们来实战开发一个稍微有一些复杂的评论模块。我们会使用使用laravel提供的Eloquent ORM框架来操作数据库。
一、业务需求分析
我们在上一章中已经创建好了项目,所以我们可以继续使用上一章中创建的项目。
评论是什么?
截几张图直观感受一下。
stackoverflow的评论(算是评论吧)
csdn的评论
极客时间的评论
各大网站的侧重点不同,评论功能也有所不同,但是核心的功能是不变的。
看上去好像很简单?我们来实际操作一下。
我们能发现,我们可以针对一条评论发起回复,回复的评论将会被展示到这条评论的下面。这个过程是一个可以无限循环下去的过程。
除此之外,还有删除、举报、点赞等功能,这些我们暂时都不去考虑。
我们根据以上内容总结一下:
-
一篇博客可以拥有多个评论。
-
一个ip可以针对一个或多个博客进行多次评论。
-
一个评论可以拥有多个子评论。
那么我们需要做什么呢?
在每一篇文章的底部都会有1个普通输入框和1个文本域输入框,分别让用户输入昵称和评论内容。评论内容是必填的,昵称可以不填。如果用户没有输入昵称,那么默认就是使用ip作为昵称。由于是创建资源,我们采用 post 方式的请求发送数据。在 url 中包含文章id,在 http 的 data 中包含 昵称 和 评论内容。
除了这两个输入框以外,文章页底部还应该有评论列表,这是一个 get 请求,需要带上文章 id。理论上讲,这里应该设计成分页的。但是我们不应该只按照理论做事,正常情况下,往往一篇博客,不可能拥有很多评论。好的API设计,应该是按照实际业务场景来设计的,所以我们不需要设计分页。
用户可以针对每一条评论或者回复进行回复。所以这是一个 post 请求,参数和发送评论的参数几乎一致,只多了一个回复目标id。
评论和回复暂时设定为不可修改和不可删除。
由于评论和评论回复不是使用特别频繁的功能,我们没有必要使用redis,直接存库即可。
上面的思路是我最早想出来的,最后我发现我目前的需求没必要设计的那么复杂,我们可以使用简单的思路来完成这个功能,最终实现的效果可以类比阮一峰老师的博客。http://www.ruanyifeng.com
他的网站评论功能大致是这样的。
所以我们只需要将评论和回复都视为评论就好了,在评论上添加一个引用。
二、数据库设计
在评论较多,回复较少时,我们可以将评论和回复都视为评论,通过建立一个父id字段来维护数据之间的关系。但是mysql是不支持嵌套查询的,那么只能通过编程语言多次对数据库进行查询,性能会较差。
但是我们的需求没必要非得是无限级的评论,可以改成引用的设计,这样做起来就简单多了。所以最简单的方式还是使用1个表。下面是我对数据库的设计。
评论表
id、博客id(blog_id)、内容(content)、用户昵称(nick_name)、引用(quote)、评论时间(create_time)
CREATE TABLE `blog`.`comment` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评论id', `blog_id` int(11) NULL DEFAULT NULL COMMENT '博客id', `content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '博客内容', `nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户昵称', `quote` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '引用', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '评论时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
其实最初的设计是两个表的,还有一个回复表,但是在做的过程中发现没有必要这么做,所以简化成了一张表,通过引用字段来建立关系。
三、Eloquent ORM框架学习
在上一章中,我们使用的是比较原始的方式进行sql操作的。这里我们需要补充一下ORM框架的知识。
什么是ORM?
对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。那么,到底如何实现持久化呢?一种简单的方案是采用硬编码方式,为每一种可能的数据库访问操作提供单独的方法。
ORM解决了什么事?
ORM解决的主要问题是对象关系的映射。域模型和关系模型分别是建立在概念模型的基础上的。域模型是面向对象的,而关系模型是面向关系的。一般情况下,一个持久化类和一个表对应,类的每个实例对应表中的一条记录,类的每个属性对应表的每个字段。
个人理解的ORM
其实在现在的常见开发场景下,服务端的底层是可以使用ORM的,但是最终仍需要组装数据。对于复杂查询,不建议使用ORM。在业务频繁变动的情况下,一旦涉及表结构的变动,代价是非常巨大的。
虽然本人在今年5月份开始,在本职工作上已经彻底不写服务端了。但仍一直关注和学习服务端的主流技术,业余时间写点小东西,或者帮朋友做点软件之类的,还是会用一些Mybatis等技术。以我我个人的经验来说,对于没有时间来认真设计的项目,ORM很难保证效率。
最终我的观点是:即使是简单的查询,我也更倾向于使用原生SQL。使用ORM的情况,大概是在团队协作时会使用。
我在第一章就提到过,要对所有技术保持开放的心态。但是,如果我们认真深入地用过某项技术很长一段时间,并且一直是按照该项技术的思想和最佳实践去践行,仍然发现这不够优雅,或者很受折磨。就应该认真思考一下,是不是这项技术本身存在一些问题。
1 模型定义
创建模型
执行php artisan make:model 模型名
命令,默认会在app根目录生成同名文件,你也可以自己创建,这个类需要继承Illuminate\Database\Eloquent\Model。
这里拿我们上面创建的评论表作为例子,在Comment中写入以下内容,下面会解释这些内容的作用。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model { // 关联表名,如果不明确表名,会按照蛇形名命去自动寻找表。 protected $table = 'comment'; // Eloquent 会自动维护时间戳,但列名必须是 created_at 和 updated_at。 const CREATED_AT = 'create_time'; // 重写 setUpdateAt 方法,不维护 update_at public function setUpdatedAt($value) { } private $blog_id; private $content; private $nick_name; }
关联表
默认使用类的复数形式「蛇形命名」来作为表名,也可以在模型上定义 table
属性来指定自定义数据表。
主键
假设每个数据表都有一个名为 id
的主键列。你可以定义一个受保护的 $primaryKey
属性来重写约定。
假设主键是一个自增的整数值,这意味着默认情况下主键会自动转换为 int
类型。如果您希望使用非递增或非数字的主键则需要设置公共的 $incrementing
属性设置为 false
。如果你的主键不是一个整数,你需要将模型上受保护的 $keyType
属性设置为 string
。
时间戳
预期你的数据表中存在 created_at
和 updated_at
。如果你不想让 Eloquent 自动管理这两个列, 请将模型中的 $timestamps
属性设置为 false
。
如果需要自定义时间戳的格式,在你的模型中设置 $dateFormat
属性。
如果你需要自定义存储时间戳的字段名,可以在模型中设置 CREATED_AT
和 UPDATED_AT
常量的值来实现。
默认属性值
在模型上定义 $attributes
属性。
2 模型检索
将每个 Eloquent 模型想象成一个强大的查询构造器 query builder ,你可以用它更快速的查询与其相关联的数据表。
Comment::all();// 查询所有数据,返回一个数组
类比原生sql
select * from comment;
附加约束
下面这段代码就是查询blog_id = 1,并且按照 create_time 字段降序,取前10条的结果。
Comment::where('blog_id', 1)
->orderBy('create_time', 'desc')
->take(10)
->get();
类比原生sql
SELECT
*
FROM
comment
WHERE
blog_id = 1
ORDER BY
create_time DESC
LIMIT 0,
10
重新加载模型
使用 fresh
和 refresh
方法重新加载模型。
区别是:
fresh
方法会重新从数据库中检索模型。现有的模型实例不受影响。
refresh
方法使用数据库中的新数据重新赋值现有模型。此外,已经加载的关系会被重新加载。
集合
Eloquent 中的 all
和 get
方法可以查询多个结果,返回一个 Illuminate\Database\Eloquent\Collection
实例。 Collection
类提供了 很多辅助函数 来处理 Eloquent 结果。
可以像数组一样遍历集合。
3 检索单个模型
可以使用 find
或 first
方法来检索单条记录。这些方法返回单个模型实例。
Comment::find(1);// 通过主键检索一个模型
Comment::where('blog_id', 1)->first();// 检索符合查询限制的第一个模型
Comment::find([1, 2]);// 使用主键数组作为参数调用 find 方法,它将返回匹配记录的集合
未查到抛出异常
findOrFail
和 firstOrFail
方法会检索查询的第一个结果,如果未找到,将抛出 Illuminate\Database\Eloquent\ModelNotFoundException
异常
如果没有捕获异常,则会自动返回 404
响应给用户。
Comment::findOrFail(1);
Comment::where('create_time', '>', '2018-11-11 00:00:00')->firstOrFail();
检索集合
可以使用 查询构造器 提供的 count
, sum
, max
, 和其他的聚合函数。这些方法只会返回适当的标量值而不是一个模型实例
Comment::where('blog_id', 2)->count('blog_id');
4 插入&更新模型
插入
要往数据库新增一条记录,先创建新模型实例,给实例设置属性,然后调用 save
方法
$comment = new Comment();
$comment->blog_id = 1;
$comment->content = 'test content.';
$comment->nick_name = '佩奇';
$comment->save();
更新
save
方法也可以用来更新数据库已经存在的模型。更新模型,你需要先检索出来,设置要更新的属性,然后调用 save
方法。同样, updated_at
时间戳会自动更新,所以也不需要手动赋值。
比如更新我们刚刚插入的那条数据。
$comment = Comment::find(4);// 刚刚那条数据的id为4
$comment->content = 'i\'m pig.';
$comment->save();
批量更新
也可以更新匹配查询条件的多个模型。
比如将用户名为 佩奇 的评论全部设为 '评论内容含有非法信息'
Comment::where('nick_name', '佩奇')
->update(['content' => '非法信息']);
update
方法接受一个键为字段名称数据为值的数组。
批量赋值
模型都默认不可进行批量赋值。
可以使用 create
方法来保存新模型,此方法会返回模型实例。在使用之前,你需要在模型上指定 fillable
或 guarded
属性。
定义好模型上的哪些属性是可以被批量赋值的。
Comment::create([
'blog_id' => 3,
'nick_name' => '乔治',
'content' => 'test .'
]);
保护属性
$fillable
可以看作批量赋值的「白名单」, 你也可以使用 $guarded
属性来实现。 $guarded
属性包含的是不允许批量赋值的数组。也就是说, $guarded
从功能上将更像是一个「黑名单」。注意:你只能使用 $fillable
或 $guarded
二者中的一个,不可同时使用
5 删除模型
可以在模型实例上调用 delete
方法来删除实例
Comment::find(4)->delete();
通过主键删除模型
如果你知道了模型的主键,你可以直接使用 destroy
方法来删除模型,而不用先去数据库中查找。 destroy
方法除了接受单个主键作为参数之外,还接受多个主键,或者使用数组,集合来保存多个主键。
Comment::destroy(3);
Comment::destroy(1, 2);//等同于 Comment::destroy([1, 2]);
ORM的知识学这么多已经足够应付70%的场景了,如果你需要学习更多内容,或者在工作中碰到其它问题,可以查阅文档:https://learnku.com/docs/laravel/5.8/eloquent/3931
四、后端代码编写
创建模型
运行php artisan make:model Comment
命令,会在app/根目录下生成Comment.php文件,修改里面的内容:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model { // 关联表名,如果不明确表名,会按照蛇形名命去自动寻找表。 protected $table = 'comment'; // Eloquent 会自动维护时间戳,但列名必须是 created_at 和 updated_at。 const CREATED_AT = 'create_time'; const UPDATED_AT = null; public function setUpdatedAt($value) { } protected $fillable = ['blog_id', 'content', 'nick_name', 'quote']; private $blog_id; private $content; private $nick_name; private $quote; }
添加路由
Route::get('getComment/{id}', 'CommentController@getComment');Route::post('postComment/{id}', 'CommentController@postComment');
创建控制器
<?php
namespace App\Http\Controllers;
use App\Comment;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function getComment($id)
{
return Comment::where('blog_id', $id)->get();
}
public function postComment(Request $request, $id)
{
if ($request->nickName == '') {
$nickName = $request->ip();
}else{
$nickName = $request->nickName;
}
$comment = new Comment();
$comment->blog_id = $id;
$comment->content = $request->input('content');// 使用input防止idea警告
$comment->nick_name = $nickName;
$comment->quote = $request->quote;
$is_success = $comment->save();
return $is_success . '';
}
}
后端代码还是非常简单的,但是第一次用ORM框架,难免会踩到一些坑。多看看文档,总能找到答案。
五、前端代码编写
我们需要对昨天的blog页面进行修改。
首先创建Comment组件和CommentList组件。
Comment组件:
CommentList组件:
import React, { Component } from "react"; import Axios from "axios"; class CommentList extends Component { state = { nickName: "佩奇", commentList: [] }; getCommentList() { Axios.get(`/getComment/${this.props.id}`).then(res => { this.setState({ commentList: res.data }); }); } quote(nickName, content) { this.props.quote(nickName, content); } componentDidMount() { this.getCommentList(); } render() { return ( <div style={{ margin: "2rem 0" }}> <div>评论:{this.state.commentList.length}条</div> {this.state.commentList.map((comment, index) => { return ( <div style={{ margin: "1rem 0" }} key={index}> <div>{comment.nick_name}说:</div> {comment.quote ? ( <div dangerouslySetInnerHTML={{ __html: comment.quote }} /> ) : null} <div>{comment.content}</div> <div> {comment.create_time} <span onClick={() => { this.quote(comment.nick_name, comment.content); }} > {" 引用"} </span> </div> </div> ); })} </div> ); } } export default CommentList;
最后需要修改一下Blog页面:
import React from "react"; import PV from "../components/PV"; import Comment from "../components/Comment"; import CommentList from "../components/CommentList"; import XSS from "../utils/xss"; class Bolg extends React.Component { state = { quote: { state: false, nickName: "", content: "" } }; quote(nickName, content) { let anchorElement = document.getElementById("comment"); if (anchorElement) { anchorElement.scrollIntoView(); } this.setState({ quote: { state: true, nickName, content } }); } render() { const id = this.props.match.params.id; return ( <div> i'm blog {id} <PV id={id}></PV> <CommentList id={id} quote={this.quote.bind(this)}></CommentList> {this.state.quote.state ? ( <div style={{ margin: "1rem", border: "1px solid pink", width: "400px" }} > <div>引用{this.state.quote.nickName} 的发言:</div> <div>{this.state.quote.content}</div> </div> ) : null} <Comment id={id} quote={ this.state.quote.state ? `<blockquote> <pre>引用${this.state.quote.nickName .replace(/</gi, "<") .replace(/>/gi, ">")}的发言:</pre> ${this.state.quote.content .replace(/</gi, "<") .replace(/>/gi, ">")} </blockquote>` : null } ></Comment> </div> ); } } export default Bolg;
你可以尝试一下这个功能。
前端还有很多可优化的点,由于时间关系我没有加prop-types的校验,也没有加入Mobx或者redux来抽离api层和管理数据。因为目前一共就3个api,还没必要做得那么复杂。
本来想把graphql也拿出来尝尝鲜的,可是我的时间好像并不那么够用。
还有第2章中的信号和进程那部分一直没有时间补上,也算是个遗憾。
这几天有时间的话,会把这几个地方补充好的。然后会把整个系列从头过一遍,重新梳理完善一下。
六、总结
至此,8天的PHP探索之旅终于告一段落了。
学习一个东西是非常快的,慢的是如何将这个知识消化,解构,重组,再输出。这个过程十分缓慢,而且很累。
不得不说这8天我几乎抽出来所有的业余时间来写这个文章,8天一共写了4万字左右,虽有很多都是代码。
首先恭喜大家,我们一起完成了整个 PHP 的学习!希望大家经过这个系列文章的学习,可以独立进行PHP应用开发。不论是个人业余开发还是实际的公司项目研发。