Home > Article > PHP Framework > Implement a blog with Laravel in 30 minutes
If you are willing to use the root user, you even only need to create a database. (However, it is not recommended. My habit is that one project corresponds to one user and one database, and root is only used to manage them)
# 创建用户 blog, 密码自定义 CREATE USER 'blog'@'%' IDENTIFIED BY '密码'; # 创建数据库 blog, 设置默认编码为utf8 CREATE DATABASE `blog` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; # 授权 授予 blog库下所有表的 所有权限 给 用户blog GRANT ALL on blog.* to 'blog'@'%';
# 进入你本地服务器用于存放网站文档的目录,输入命令 composer create-project --prefer-dist laravel/laravel blog
In the following, "/" means the root directory of the laravel framework
# 数据库配置 DB_CONNECTION=mysql #类型 DB_HOST=127.0.0.1 #ip DB_PORT=3306 #端口 DB_DATABASE=数据库名 DB_USERNAME=用户名 DB_PASSWORD=密码
composer require caouecs/laravel-lang
Then put /vendor/caouecs/src/zh-CN/ under /resources/lang/# 时区 'timezone' => 'Asia/Shanghai', # 语言 'locale' => 'zh-CN',
You may not have seen the above three terms, but the files related to them are stored under /database/ : From the name of this folder, you may have guessed: these three files are used to operate the database.
In the above, we only created the database and did not create a data table. Now let’s confirm our data table
The project is a personal blog, so only the blogger can publish and delete it , modify the blog. Other users can view the blog and post comments.
Use Migration to create these 3 data tables
php artisan make:migration create_blogs --create=blogs
Blog table, php artisan make:migration create_comments --create=comments
Comment tablephp aritsan is laravel built-in You can enter the command directly on the console, and you will be prompted on the console for the next command you can enter.
Above we used make:migration to help us create a migration file. --create is a parameter, which tells this command to help us create a migration file for creating a data table
Why not create a user table? Open /database/migrations/ and you will find that there is a users and password_resets table created in 2014. This comes with the framework.
Edit these two migration files
// 首先类定义中,有两个方法,up()可以理解为正向操作:创建表,而 down()可以理解为回滚操作:删除表。 // up() Schema::create('blogs', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('content'); $table->timestamps(); }); // down() 已经自动帮我们写好了。
// up() Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->string('content'); $table->integer('blog_id'); //这条评论是针对哪一篇博客的? $table->integer('user_id'); //这条评论是哪一位用户发送的? $table->timestamps(); });
执行迁移: 1、确保你的 /.env 配置正确 2、确保你的数据库可以正常使用 3、确保数据库中没有数据表或者没有和users blogs comments重名的数据表 php artisan migrate
打开数据库(你可以任选一款数据库管理工具,或者直接使用mysql的命令行),打开数据库 blog ,你会发现有以下表
主要解释一下 migrations 表:
php artisan migrate:rollback
即回滚数据库,将会执行批次batch最大的记录的那些迁移文件的 down() 方法。migration 的作用:1、帮我们省略了去写sql语句的麻烦,2、让我们对数据库可以进行有效的版本控制。
在日常的开发中,我们需要很多模拟的数据进行测试,模型工厂的作用就是帮我们快速的,随机的生成这些数据。
php artisan make:factory BlogFactory --model=Blog
, 关于评论表的模型工厂请自己写。php artisan make:model Blog
, 关于评论表的模型请自己写。细心的你可能发现了,我们的数据表和模型的名字是有区别的:数据表为“小写复数形式”,而模型名为“大写单数形式”。 创建的模型都存在于 /app/ 下。
// 使用 Faker 类为我们提供的生成随机伪造数据的方法生成数据 return [ 'title' => $faker->name, 'content' => $faker->text, ];
return [ 'content' => $faker->text, 'blog_id' => 1, 'user_id' => 1, ];
php artisan tinker
, 当命令提示符变为 ">>>" 时,你就处于tinker模式下了,此时你可以输入php代码,或者调用laravel提供的全局函数,甚至引用一个类,调用它的静态方法或者实例化它。factory(App\Blog::class)->make()
此时屏幕上会显示,它给你模拟出来的一个虚拟数据数组。factory(App\Blog::class, 100)->create()
打开数据库,您会发现100条标题和内容都无关紧要,但是对我们快速开发特别有用的测试数据已经存放在数据库中了。php artisan make:seeder UserTableSeeder
,针对博客表和评论表的Seeder创建命令自己写。... use App\User; // 在 class 关键字前面,引用一下 User 模型 class ... public function run() { factory(App\User::class, 50)->create(); //向users表中插入50条模拟数据 $user = User::find(1); //插入完后,找到 id 为 1 的用户 $user->name = "najiuyuanzou"; //设置 用户名 $user->email = "najiuyuanzou@test.com"; //设置 邮箱 $user->password = bcrypt('liuhaoyu'); //设置 密码 $user->save(); //保存 }
在这里我们明确1号用户为真实的可用的管理员用户!所以我们设置一下它的 用户名 邮箱 以及密码
其余的Seeder我们可以只插入模拟的数据即可。
现在回过头来,编辑 DatabaseSeeder
public function run() { // $this->call(UsersTableSeeder::class); // 这里给我们举例了如何引用其他Seeder $this->call(UserTableSeeder::class); $this->call(BlogTableSeeder::class); $this->call(CommentTableSeeder::class);// 这里有先后关系需要注意一下: 评论n : 1文章/用户,所以应该把它写在最后 }
php artisan migrate:refresh --seed
=> 查看数据库,发现数据库重置了,并且 users blogs comments 每张表都有很多虚拟的数据。也许你到这里会觉得这还不如你写sql语句。但是请相信我,等你熟练掌握使用这些东西之后,你的开发速度会非常快!(毕竟你不需要再 "INSERT INTO table values ()" 复制粘贴修改100遍了)
php artisan make:auth
好吧你可能很懵逼,但这就是Laravel的厉害之处,那个2014年就建好的migration迁移文件可不是个摆设。它就是通过操作users表来实现注册登陆等等的。
php artisan make:auth
到底干了哪些事情呢?
Auth::routes(); //这是用户操作相关的路由 Route::get('/home', 'HomeController@index')->name('home'); //这是主页的路由
我们改良一下它自动为我们生成的东西
// Route::action('uri', 'Controller@function'); Route::get('/', 'HomeController@index'); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home');
/** * 这里这个构造函数调用了 中间件auth 对我们进行权限认证 * 即要求我们必须登陆才可以访问该控制器的其他方法 * 有两种解决方法,一直是在 $this->middleware('auth')->except('你要排除权限认证的方法'),比如 ...->except('index') * 另一种是直接干掉这个函数(我们确定这个控制器就只是来展示首页的,那么就干掉它吧) */ // public function __construct() // { // $this->middleware('auth'); // }
return view('视图名称')
来抓取视图显示在页面上,现在打开浏览器访问主页,你就可以看得到 home.blade.php 中的内容了,我们看看 /resources/views/home.blade.php 的内容: 重点: @extens @section
{{-- 内容不重要我们等下要改,先来看下 @符号 的作用 --}} {{-- @extends继承其他模板 --}} @extends('layouts.app') {{-- 这里的layouts.app => /resources/views/layouts/app.blade.php --}} {{-- @section 填充在布局模板上用 @yield 标注的占位符 --}} @section('content') {{-- 你可以在 /resources/views/layouts/app.blade.php 看到 @ yield('content')标注的占位符 --}} ... 这里面是html内容 @endsection
Auth为我们生成的整个视图模板的逻辑: layouts/app.blade.php 为布局模板,其他模板都继承该模板。
@extends('layouts.app') @section('content')<div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">欢迎!这里是 “那就远走” 的个人博客</div> <div class="card-body"> 如无牵挂,那就远走。 {{-- 这里等下要添加一个跳转到展示文章列表页面的按钮 --}} </div> </div> </div> </div> </div> @endsection
# 首先布局模板我们需要把 brand 登陆、注册这些东西改一改 {{ config('app.name', 'Laravel') }} => 我的博客 //注意这里有一个 config('app.name') 该函数其实是读取的 /.env 里的 APP_NAME 值,且默认值为 'Laravel' ,也就是说,你改 APP_NAME 也可以改这里显示的值,不过我嫌麻烦,直接查找替换了。 {{ __('Login') }} => 登陆 {{ __('Register') }} => 注册 {{ __('Logout') }} => 退出 # 然后登陆模板: {{ __('Login') }} => 登陆 {{ __('E-Mail Address') }} => 邮箱 {{ __('Password') }} => 密码 {{ __('Remember Me') }} => 记住我 然后我们把 ForgetPassword 那个按钮给干掉吧(这个找回密码的功能需要一个SMTP服务的邮箱才能实现,现在暂时不弄) # 然后注册模板 {{ __('Register') }} => 注册 {{ __('Name') }} => 昵称(用户名) {{ __('Confirm Password') }} => 密码确认 # 有可能有说漏的,反正自己看着页面上的英文查找替换成中文就可以了。
Route::get('home', 'HomeController@home')
=> 即表示,你输入 "http://localhost/blog/public/home" 是以GET的请求方式去请求 HomeController 的 home() 方法。return view()
调用和渲染它,最终展示给网站访客。我们写的程序,除了前台好看的界面,就是后台的程序,而后台的程序无非就是“增删改查”以及“花式增删改查”罢了。
因此,仔细想想,对于一张数据表的操作,我们通常就需要这些行为:1、一个分页展示所有数据的列表 2、一个添加数据的功能 3、一个编辑数据的功能 4、一个显示单条数据详细信息的功能 5、一个删除功能。
创建一个资源控制器,一次性帮我们生成能实现上面5个功能的方法 php artisan make:controller BlogController --resource --model=Blog
( --resouce生成的控制器为资源控制器即自带 CURD增删改查 所有方法的控制器 ) ( --model 是让生成的控制器在参数列表中自动帮我们完成依赖注入生成实际变量 )
根据 三_1 阶段的说法,我们其实需要设置很多路由,来对应生成的 BlogController 下的各种方法,Laravel已经帮我们想到了所以它给我们提供了这样一种方法配置路由,编辑 /routes/web.php ,在最后面添加这么一句 Route::resource('blog', 'BlogController');
有一个命令可以看到当前所有生效的路由 php artisan route:list
,控制台自动弹出的表格中,Method 表示 请求方法,即Route::这里的方法()
,URI 表示 请求地址,即 Route::action('这里就是uri')
, name 表示 路由别名,可以通过 Route::action('uri', 'Controller@function')->name('这里可以定义路由别名')
。
你会发现,有7条关于 blog 的路由,这就是 Route::resource('blog', 'BlogController')
帮我们生成的。(5个功能7条是因为 添加和编辑多了2条载入视图的路由)
完成增删改查吧: 首先完成 BlogController@index : 展示列表
{{-- 上面说过这里会添加一个按钮 --}} <a href="{{ route('blog.index') }}" class="btn btn-lg btn-block btn-primary">点击这里查看我的博客</a>
<?php namespace App\Http\Controllers; use App\Blog; //这里是使用命令创建控制器时,通过 --model=Blog 自动帮我们生成的 use Illuminate\Http\Request; class BlogController extends Controller{ public function index() { // 查询数据,并且让查询结果是一个可分页对象 $blogs = Blog::orderBy('created_at', 'desc') // 调用 Blog模型 的静态方法 orderBy('根据created_at字段', '倒叙排序') ->paginate(6); // -> 链式操作:paginate(6) 即数据没页6条 // 跳转到视图并传值 return view('blog.index', [ //第一个参数是说,视图模板是 /resources/views/blog/index.blade.php 'blogs' => $blogs, //这里是说,我们给视图传递一个叫 $'blogs'的变量,值是前面我们查询的数据,也叫$blogs。 ]); // view() 的第二参数也可以使用 view(..., compact('blogs')) }
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="card"> <div class="card-header">这是页面小标题</div> <div class="card-body"> 这里是内容 </div> </div> </div> </div> @endsection
这就是我们所有页面的布局,可以复制一份,作为模板。
<div class="container"> <div class="row justify-content-center"> <div class="card"> <div class="card-header">文章列表</div> <div class="card-body"> <table class="table table-hover table-bordered"> <thead class="bg-info"> <tr> <th>文章标题</th> <th>发布时间</th> <th>相关操作</th> </tr> </thead> <tbody> {{-- 这里通过 @foreach 遍历数据 --}} @foreach ($blogs as $blog) <tr> <td>{{ $blog->title }}</td> <td>{{ $blog->created_at }}</td> <td></td> </tr> @endforeach </tbody> <tfoot> {{-- 这里通过 $blogs->links() 来显示分页按钮 --}} {{ $blogs->links() }} </tfoot> </table> </div> </div> </div> </div>
{{-- route('路由别名') 在视图上就是一个指向 BlogController@create 的链接 --}} <a href="{{ route('blog.create') }}" class="dropdown-item"> 添加文章 </a>
public function create(){ return view('blog.create'); //载入视图 }
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="card"> <div class="card-header">添加文章</div> <div class="card-body"> {{-- from.method="POST" action="通过 route()函数读取路由别名 " --}} <form method="POST" action="{{ route('blog.store') }}"> {{-- 声明 csrf 令牌 --}} @csrf <div class="form-group"> <label for="title">文章标题</label> <input type="text" class="form-control" id="title" placeholder="请输入文章标题" name="title"> </div> <div class="form-group"> <label for="content">文章内容</label> <textarea id="content" cols="30" rows="10" class="form-control" name="content"></textarea> </div> <button class="btn btn-primary" type="submit">发布新文章</button> </form> </div> </div> </div> </div> @endsection
所谓跨站请求伪造,可以理解为来自于其他ip的表单,恶意请求我们的服务器。Laravel提供了一种防范这种攻击的手段,即将自己的路由隐藏起来,只有带有 @csrf 声明的表单可以找得到接收表单信息的路由
public function store(Request $request) //这里的 $request 是通过依赖注入的方法实例化的 Request 类的对象,包含的有所有请求的信息 { // 我们只需要调用 Blog模型 的静态方法 create() 插入 $request->post() 数据即可 $blog = Blog::create($request->post()); //改方法的返回值是新插入的数据生成的对象 // redirect() 页面重定向 return redirect()->route('blog.show', $blog); // 这里我们将 $blog 作为参数请求 BlogController@show }
// 可填字段白名单 protected $fillable = [ 'title', 'content' ];
再次提交,页面一片空白,是因为我们的 BlogController@show 方法还没有写,不过你可以注意到地址栏已经发生了改变。
完成 show 方法
public function show(Blog $blog) //这里已经通过依赖注入的形式帮我们实例化了 $blog { return view('blog.show', [ 'blog' => $blog, //直接将$blog传给视图进行渲染 ]); }
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="card"> <div class="card-header">文章详情</div> <div class="card-body"> <h1 class="text-center">{{ $blog->title }}</h1> <p>发布时间<small>{{ $blog->created_at }}</small></p> <hr> <p> {{ $blog->content }} </p> </div> </div> </div> </div> @endsection
刷新页面,文章就显示出来了。
完成我们的编辑入口链接: 在 ../blog/index.blade.php & show.blade.php 中合理的位置添加一个编辑按钮
<a href="{{ route('blog.edit', $blog->id) }}" class="btn btn-info">编辑文章</a>
public function edit(Blog $blog){ return view('blog.edit', [ 'blog' => $blog, ]); }
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="card"> <div class="card-header">编辑文章</div> <div class="card-body"> {{-- action需要声明当前编辑的文章编号$blog->id --}} <form method="POST" action="{{ route('blog.update', $blog->id) }}"> {{-- 声明 csrf 令牌 --}} @csrf {{-- 伪造 PATCH 方法 --}} @method("PATCH") <div class="form-group"> <label for="title">文章标题</label> <input type="text" class="form-control" id="title" placeholder="请输入文章标题" name="title" value="{{ $blog->title }}"> </div> <div class="form-group"> <label for="content">文章内容</label> <textarea id="content" cols="30" rows="10" class="form-control" name="content">{{ $blog->content }}</textarea> </div> <p>发表于<small>{{ $blog->created_at }}</small></p> <p>修改于<small>{{ $blog->updated_at }}</small></p> <button class="btn btn-primary" type="submit">确认编辑</button> </form> </div> </div> </div> </div> @endsection
public function update(Request $request, Blog $blog){ $blog->update($request->post()); //调用 $blog对象->update(更新数据组成的数组) 更新 return redirect()->route('blog.show', $blog); }
<a href="javascript:deleteConfirm({{ $blog->id }});" class="btn btn-danger btn-sm">删除文章</a> {{-- 因为删除也需要 csrf 令牌认证,所以弄个表单 --}} <form method="POST" action="{{ route('blog.destroy', $blog->id) }}" id="delete-blog-{{ $blog->id }}"> @csrf {{-- 这里伪造DELETE请求 --}} @method("DELETE") </form>
<script> function deleteConfirm(id) { var confirm = window.confirm('确认要删除这篇文章吗?'); if(confirm === true) { $("#delete-blog-" + id).submit(); //提交表单 }else { window.alert('你选择不删除!'); } } </script>
public function destroy(Blog $blog){ $blog->delete(); return redirect()->route('blog.index'); //跳转到首页 }
完善和优化
首先无论增删改查操作,成功后我们没有任何提示,我们使用 session 闪存方法消息吧:
_
下划线开头<div class="container"> {{-- 遍历 success danger 这两个我们等会会在 session->flash() 方法中设置的"key" --}} @foreach (['success', 'danger'] as $msg) {{-- 当key存在的时候,证明我们给 session flash 闪存里面装载了一次提示信息,那么就显示提示信息 --}} @if (session()->has($msg)) <div class="alert alert-{{ $msg }}"> <ul> <li>{{ session()->get($msg) }}</li> </ul> </div> @endif @endforeach </div>
{{-- 在导航下面,内容上面导入 --}}@include('components._message')
$blog->delete(); session()->flash('success', '删除文章成功!'); //装载session闪存 return redirect()->route('blog.index');
然后有个问题,就是在于,我们这是一个个人博客,所以只有我们自己可以对博客文章进行增删改,而用户只可以进行查看。因此我们需要:
public function __construct(){ $this->middleware('auth')->except('index'); }
// 因为比较简单,所以我们不用Policy进行认证,我会在以后的教程里面教大家如何使用Policy策略进行权限认证 // 这里我们就使用判断当前用户在数据表中信息的主键id是不是1即可(因为我们在Seeder里面把编号为1的用户设置为了可用的管理员账号) // 1、在代码开头引用 Auth // 2、在方法内先判断一下是不是 1号用户 if(Auth::user()->id != 1) { // Auth::user() 获取当前用户信息 -> id获取属性id(主键) session()->flash('danger', '抱歉,只有博主才可以新增文章!'); return redirect()->back(); }
针对博客的增删改查在这里就结束了。
session()->flash()
装载闪存信息,用一个组件html片段加载信息,最后用@include()
在模板上加载这个html组件。php artisan make:controller CommentController --model=Commment
Route::resource('comment', 'CommentController', ['only' => 'store']);
其实我们可以定义一条
Route::post('comment', 'CommentController@store')
路由,但是为什么要写资源路由呢?因为我要告诉你资源路由可以用['onlu'=>'操作']
让其只支持一种操作:)
<form method="POST" action="{{ route('comment.store') }}"> @csrf <input type="hidden" name="user_id" value="{{ Auth::user()->id }}"> <input type="hidden" name="blog_id" value="{{ $blog->id }}"> <div class="form-group"> <label for="content"></label> <textarea id="content" class="form-control" cols="30" rows="10" name="content">您对这篇文章有什么看法呢?</textarea> </div> <button class="btn btn-primary" type="submit">发表评论</button> </form>
public function store(Request $request){ Comment::create($request->post()); session()->flash('success', '评论成功!'); return redirect()->back(); }
protected $fillable = [ 'content', 'user_id', 'blog_id' ];
// 绑定1:n关系 public function comments() { return $this->hasMany('App\Comment'); // 1 hasMany n }
public function blog() { return $this->belongsTo('App\Blog'); // n belongsTo 1 }
$blog->comments
来获取属于这篇文章的评论// 查询评论 $comments = $blog->comments; // 视图渲染 return view('blog.show', [ 'blog' => $blog, 'comments' => $comments, //把评论也传过去 ]);
<h3>评论</h3> <ul> @foreach ($comments as $comment) <li><small>{{ $comment->userName() }} 评论说:</small>“ {{ $comment->content }} ”</li> @endforeach</ul>
use App\User; //引用模型 // 根据 user_id 获取用户名 public function userName() { return User::find($this->user_id)->name; //这里通过当前对象的 user_id 获取 user对象, 然后指向->name属性 }
在博客中,我们就没有使用验证,那是因为项目定位是一个个人博客,能够操纵博客增删改的只有我们自己。而评论则是只要有人注册账号,就可以评论了,所以我们应该对评论进行一些校验以防恶意攻击。
新建一个请求Request php artisan make:request CommentRequest
新建的请求位于 app\Http\Requests\ 下,编辑 CommentRequest
// 1、 开启授权 public function authorize(){ return true; //如果返回false则所有请求都无法生效,会告诉你没有授权(其实在这里面我们是需要去进行判断的,但是这里的逻辑很简单:只有登陆才能查看文章详情,才能看到文章详情下面发表评论的表单,才能发表评论。)所以我们这里直接 return true;}// 2、 编辑规则public function rules(){ return [ 'content' => 'required|min:15|max:100', //这里你可以定义规则我的是:必填、最少15字、最多100字 ]; }
public function store(CommentRequest $request) // 这将 Request 改成 CommentRequest 就会自动调用 CommentRequest@rules 来校验请求的数据了 { Comment::create($request->post()); session()->flash('success', '评论成功!'); return redirect()->back(); }
{{-- 样式里面加一个判断,判断是否有关于content的错误有的话给样式给文本域加一个红边边 --}} <textarea id="content" class="form-control {{ $errors->has('content') ? ' is-invalid' : '' }}" cols="30" rows="10" name="content">你对文章有什么看法呢?</textarea> {{-- 如果有错误,再显示一个小的错误提示信息 --}} @if ($errors->has('content')) <span class="invalid-feedback"> <strong>{{ $errors->first('content') }}</strong> </span> @endif
'content' => '内容', //这里就是配置字段的中文名,你把它改成评论即可。
{{-- 判断是否有错误 --}} @if (count($errors) > 0) <div class="alert alert-danger"> {{-- 遍历所有错误 --}} @foreach ($errors->all() as $error) <ul> {{-- 打印错误 --}} <li> {{ $error }}</li> </ul> @endforeach </div>
@endif
@include('components._error')
['only'=>'store']
让资源路由只暴露指向 CommentController@store 的路由hasMany() & belongsTo()
绑定模型之间的1:n关系。然后通过文章->评论+s;
的方法直接获取了属于某篇文章的所有评论。content
字段来判断是否是表单提交的评论有问题,然后修改文本域的样式并且在下方用一个小的提示span显示错误提示信息php artisan migrate:rollback
也许文字很多,但是真正的代码可能只有不到100行,你如果熟练掌握,可能不需要30分钟,甚至10分钟,你就可以开发出这样一个博客了。
现在请告诉我,它是否配得上 “优雅” 的两字?:)
I hope everyone can like, learn and promote Laravel. If you are willing to put in 0.01 points more effort than learning thinkphp5, I think this framework is very simple.
If you still hate how big it is, I recommend the Lumen framework to you.
The above is the detailed content of Implement a blog with Laravel in 30 minutes. For more information, please follow other related articles on the PHP Chinese website!