作为 PHP 博客实战项目的终结篇,我们将在后台管理系统为专辑、文章、消息模块添加增删改查功能,来完成内容生产和消费的闭环。
1、后台首页重构
在此之前,我们需要先改造后台首页视图,通过博客功能模块替代默认的示例代码。
控制器改造
在 app/http/controller/admin
目录下新建 AdminController
作为管理后台控制器的基类,并且初始化全局变量:
<?php
namespace AppHttpControllerAdmin;
use AppHttpControllerController;
use AppModelMessage;
class AdminController extends Controller
{
protected $messages;
protected $authUser;
protected $itemsPerPage = 15;
public function __construct()
{
parent::__construct();
if (!$this->session->has('auth_user')) {
redirect('/login');
}
$this->authUser = $this->session->get('auth_user');
$this->messages = Message::orderBy('created_at', 'desc')->limit(3)->get();
}
}
我们将用户认证校验逻辑放到这个后台控制器基类的构造函数中,并且从 Session 中获取用户实例,以及消息列表信息(用于渲染顶部导航栏的消息数据)。
然后让 DashboardController
继承自这个基类:
<?php
namespace AppHttpControllerAdmin;
class DashboardController extends AdminController
{
public function index()
{
$pageTitle = '管理后台 - ' . $this->siteName;
$this->view->render('admin/index.php', [
'pageTitle' => $pageTitle,
'siteName' => $this->siteName,
'user' => $this->authUser,
'messages' => $this->messages
]);
}
}
并将认证用户和消息对象传入视图模板。
视图模板改造
对于后台首页视图模板的主体部分代码不做改动,调整 nav.php
部分视图模板代码如下:
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
... // 未修改代码省略
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
... // 未修改代码省略
<!-- Nav Item - Messages -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages -->
<!--<span class="badge badge-danger badge-counter">7</span>-->
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="messagesDropdown">
<h6 class="dropdown-header">
消息中心
</h6>
<?php foreach ($messages as $message):?>
<a class="dropdown-item d-flex align-items-center" href="/admin/message/<?=$message->id?>">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="<?=get_gravatar($message->email, 60)?>" alt="">
<div class="status-indicator bg-success"></div>
</div>
<div class="font-weight-bold">
<div class="text-truncate"><?=$message->content?></div>
<div class="small text-gray-500"><?=$message->name?></div>
</div>
</a>
<?php endforeach;?>
<a class="dropdown-item text-center small text-gray-500" href="/admin/messages">更多消息</a>
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small"><?=$user->name;?></span>
<img class="img-profile rounded-circle" src="/image/me.jpg">
</a>
... // 未修改代码省略
</li>
</ul>
</nav>
<!-- End of Topbar -->
以及 sidebar.php
部分视图模板代码如下:
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="/admin">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-laugh-wink"></i>
</div>
<div class="sidebar-brand-text mx-3"><?=$siteName?></div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item active">
<a class="nav-link" href="/admin">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>管理后台</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo">
<i class="fas fa-fw fa-folder"></i>
<span>专辑</span>
</a>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="/admin/albums">列表</a>
<a class="collapse-item" href="/admin/album/new">新增</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseUtilities" aria-expanded="true" aria-controls="collapseUtilities">
<i class="fas fa-fw fa-feather"></i>
<span>文章</span>
</a>
<div id="collapseUtilities" class="collapse" aria-labelledby="headingUtilities" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="/admin/posts">列表</a>
<a class="collapse-item" href="/admin/post/new">新增</a>
</div>
</div>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Addons
</div>
<!-- Nav Item - Charts -->
<li class="nav-item">
<a class="nav-link" href="/admin/messages">
<i class="fas fa-fw fa-comment-dots"></i>
<span>消息</span>
</a>
</li>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->
访问新的后台首页
运行 composer dump-auto
让修改代码后引起的自动加载变化生效,重新刷新后台,就可以看到新的后台首页视图了:
2、专辑模块增删改查实现
接下来,我们就可以通过为专辑、文章、消息模块实现增删改查功能,来补全上面侧边栏链接点击后渲染的页面了。
这里我们以专辑为例进行演示。
路由 & 控制器
首先在 app/routes/web.php
中注册对应的路由:
$router->register('get', 'admin/albums', 'AdminAlbumController@index');
$router->register(['get', 'post'], 'admin/album/new', 'AdminAlbumController@add');
$router->register(['get', 'post'], 'admin/album/edit', 'AdminAlbumController@edit');
$router->register(['post'], 'admin/album/delete', 'AdminAlbumController@delete');
然后在 app/http/controller/admin
目录下创建对应的控制器 AlbumController
,以及对应列表页、新增/修改表单、删除处理逻辑:
<?php
namespace AppHttpControllerAdmin;
use AppHttpExceptionValidationException;
use AppHttpResponse;
use AppModelAlbum;
use SymfonyComponentHttpFoundationFileUploadedFile;
class AlbumController extends AdminController
{
public function index()
{
$page = intval($this->request->get('page'));
$pageTitle = '管理后台 - 专辑列表';
$albumsTotalNums = Album::count();
$albumsTotalPage = ceil($albumsTotalNums / $this->itemsPerPage);
if ($page <= 0) {
$page = 1;
}
if ($page > $albumsTotalPage) {
$page = $albumsTotalPage;
}
$albums = Album::orderBy('id', 'desc')->offset(($page - 1) * $this->itemsPerPage)->limit($this->itemsPerPage)->get();
$this->view->render('admin/album/index.php', [
'pageTitle' => $pageTitle,
'siteName' => $this->siteName,
'user' => $this->authUser,
'messages' => $this->messages,
'page' => $page,
'total' => $albumsTotalPage,
'albums' => $albums
]);
}
public function add()
{
$pageTitle = '管理后台 - 新增专辑';
if ($this->request->getMethod() == 'GET') {
$this->view->render('admin/album/new.php', [
'pageTitle' => $pageTitle,
'siteName' => $this->siteName,
'user' => $this->authUser,
'messages' => $this->messages,
]);
} else {
$title = $this->request->get('title');
$summary = $this->request->get('summary');
$image = $this->request->files->get('image');
$this->validate($title, $summary, $image);
$album = new Album();
$album->title = $title;
$album->summary = $summary;
$album->image = '/image/' . $image->getClientOriginalName();
if ($album->save()) {
redirect('/admin/albums');
} else {
$this->view->render('admin/album/new.php', [
'pageTitle' => $pageTitle,
'siteName' => $this->siteName,
'user' => $this->authUser,
'messages' => $this->messages,
'error' => '专辑保存失败,请重试',
'title' => $title,
'summary' => $summary
]);
}
}
}
public function edit()
{
$pageTitle = '管理后台 - 编辑专辑';
$id = $this->request->get('id');
$album = Album::findOrFail($id);
if ($this->request->getMethod() == 'GET') {
$this->view->render('admin/album/edit.php', [
'pageTitle' => $pageTitle,
'siteName' => $this->siteName,
'user' => $this->authUser,
'messages' => $this->messages,
'album' => $album
]);
} else {
$title = $this->request->get('title');
$summary = $this->request->get('summary');
$image = $this->request->files->get('image');
$origin_image = $this->request->get('origin_image');
$this->validate($title, $summary, $image, $origin_image);
$album->title = $title;
$album->summary = $summary;
if (!empty($image)) {
$album->image = '/image/' . $image->getClientOriginalName();
}
if ($album->save()) {
redirect('/admin/albums');
} else {
$this->view->render('admin/album/edit.php', [
'pageTitle' => $pageTitle,
'siteName' => $this->siteName,
'user' => $this->authUser,
'messages' => $this->messages,
'error' => '专辑保存失败,请重试',
'title' => $title,
'summary' => $summary,
'album' => $album
]);
}
}
}
public function delete()
{
$id = $this->request->get('id');
$album = Album::findOrFail($id);
$album->delete();
redirect('/admin/albums');
}
protected function validate()
{
$params = func_get_args();
$title = $params[0];
$summary = $params[1];
$image = $params[2];
$origin_image = null;
if (isset($params[3])) {
$origin_image = $params[3];
}
if (empty($title)) {
throw new ValidationException('专辑名称不能为空');
}
if (empty($summary)) {
throw new ValidationException('专辑简介不能为空');
}
if (empty($image) && empty($origin_image)) {
throw new ValidationException('专辑图片不能为空');
}
if (empty($image)) {
return;
}
if (!($image instanceof UploadedFile) || !$image->isValid()) {
throw new ValidationException('专辑图片上传出错,请重试');
}
if ($image->getSize() > 1 * 1024 * 1024) {
throw new ValidationException('上传图片不能超过 1M');
}
if (!in_array($image->getClientMimeType(), ['image/png', 'image/jpeg', 'image/gif'])) {
throw new ValidationException('仅支持上传 png、jpg、gif 格式图片');
}
$path = $this->container->resolve('app.basePath') . 'public/image';
$image->move($path, $image->getClientOriginalName());
}
}
专辑相关视图模版
接下来,我们分别为专辑列表页、新增专辑、修改专辑表单编写视图模板。在 resources/views/admin
目录下新建 album
子目录用来存放专辑相关视图模板。
专辑列表页
resources/views/admin/album/index.php
:
<?php include __DIR__ . '/../header.php';?>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<?php include __DIR__ . '/../sidebar.php';?>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<?php include __DIR__ . '/../nav.php';?>
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- DataTales Example -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">专辑列表</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>封面图</th>
<th>名称</th>
<th>介绍</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($albums as $album):?>
<tr>
<td><?=$album->id?></td>
<td>
<?php if ($album->image): ?>
<img src="<?=$album->image?>" class="img-thumbnail" style="width: 15em;">
<?php endif;?>
</td>
<td><?=$album->title?></td>
<td><?=$album->summary?></td>
<td>
<a href="/admin/album/edit?id=<?=$album->id?>" role="button" class="btn btn-success">编辑</a>
<a href="#" data-toggle="modal" data-target="#deleteModal" role="button" class="btn btn-danger btn-delete" data-id="<?=$album->id?>">删除</a>
</td>
</tr>
<?php endforeach;?>
</tbody>
</table>
</div>
<?php
$pageType = 'albums';
include __DIR__ . '/../pagination.php';
?>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<?php
$itemType = 'album';
include __DIR__ . '/../delete.php';
include __DIR__ . '/../footer.php';
?>
这里我们还引入了一个局部组件 pagination.php
,它位于 album
的上一级目录 admin
下,用于渲染列表页分页组件:
<!--分页-->
<?php if ($total > 1):?>
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item <?php if ($page == 1): echo 'disabled'; endif;?>">
<a class="page-link" href="/admin/<?=$pageType?>?page=<?=($page - 1)?>" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<?php for ($i = 1; $i <= $total; $i ) { ?>
<li class="page-item <?php if ($page == $i): echo 'active'; endif;?>">
<a class="page-link" href="/admin/<?=$pageType?>??page=<?=$i?>"><?=$i?></a>
</li>
<?php } ?>
<li class="page-item <?php if ($page >= $total): echo 'disabled'; endif;?>">
<a class="page-link" href="/admin/<?=$pageType?>?=<?=($page 1)?>" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
<?php endif;?>
新增专辑表单
resources/views/admin/album/new.php
:
<?php include __DIR__ . '/../header.php';?>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<?php include __DIR__ . '/../sidebar.php';?>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<?php include __DIR__ . '/../nav.php';?>
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- DataTales Example -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">新增专辑</h6>
</div>
<div class="card-body">
<form action="/admin/album/new" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="title">标题</label>
<input type="text" name="title" class="form-control" id="title" value="<?php echo isset($title) ? $title : ''?>">
</div>
<div class="form-group">
<label for="summary">简介</label>
<textarea class="form-control" id="summary" rows="3" name="summary"><?php echo isset($summary) ? $summary : ''?></textarea>
</div>
<div class="form-group">
<label for="feature_image">封面图</label>
<input type="file" class="form-control-file" id="feature_image" name="image">
</div>
<?php if (!empty($error)): ?>
<div class="alert alert-danger" role="alert">
<?=$error?>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<?php include __DIR__ . '/../footer.php';?>
修改专辑表单
修改表单和新增表单非常类似,其实是可以合并到一个视图的(留给大家作为课后作业去实现)。
resources/views/admin/album/edit.php
:
<?php include __DIR__ . '/../header.php';?>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
<?php include __DIR__ . '/../sidebar.php';?>
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
<?php include __DIR__ . '/../nav.php';?>
<!-- Begin Page Content -->
<div class="container-fluid">
<!-- DataTales Example -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">编辑专辑</h6>
</div>
<div class="card-body">
<form action="/admin/album/edit?id=<?=$album->id?>" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="title">标题</label>
<input type="text" name="title" class="form-control" id="title" value="<?=$album->title?>">
</div>
<div class="form-group">
<label for="summary">简介</label>
<textarea class="form-control" id="summary" rows="3" name="summary"><?=$album->summary?></textarea>
</div>
<div class="form-group">
<label for="feature_image">封面图</label>
<input type="file" class="form-control-file" id="feature_image" name="image">
<?php if ($album->image):?>
<img src="<?=$album->image?>" class="img-thumbnail" style="width: 15em;">
<input type="hidden" name="origin_image" value="<?=$album->image?>">
<?php endif;?>
</div>
<?php if (!empty($error)): ?>
<div class="alert alert-danger" role="alert">
<?=$error?>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
</div>
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
<?php include __DIR__ . '/../footer.php';?>
删除功能实现
删除功能是在列表页点击删除按钮发送 Ajax 请求来实现的,我们留意到 album/index.php
列表页有一段删除按钮的 HTML 代码:
<a href="#" data-toggle="modal" data-target="#deleteModal" role="button" class="btn btn-danger btn-delete" data-id="<?=$album->id?>">删除</a>
这段代码会弹出一个删除模态框,对应的 HTML 代码位于 resources/views/admin/delete.php
中:
<!-- Logout Modal-->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">确定要删除?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">点击下面的 "删除" 按钮删除选定内容</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">取消</button>
<button class="btn btn-primary" type="submit" id="deleteItemBtn">删除</button>
<form action="/admin/<?=$itemType?>/delete" method="post" id="deleteItemForm">
<input type="hidden" name="id" value="" id="deleteItemId">
</form>
</div>
</div>
</div>
</div>
我们在 resources/js/admin.js
末尾添加对应的 Ajax 请求代码完成专辑删除功能:
$(function () {
$('.btn-delete').on('click', function () {
$('#deleteItemId').val($(this).attr('data-id'));
})
$('#deleteItemBtn').on('click', function () {
$('#deleteItemForm').submit();
});
});
运行 composer dump-auto
和 npm run dev
让修改代码生效。
测试专辑增删改查功能
在侧边栏点击专辑列表就可以看到如下渲染的视图效果了:
点击侧边栏中的新增专辑链接就可以进入新增专辑页面:
在列表页点击编辑按钮,就可以编辑对应的专辑记录:
最后,我们可以在专辑列表页通过删除按钮删除对应的专辑,删除前会弹出确认模态框,确认之后就会删除这个专辑:
3、其它模块增删改查实现
文章和消息的增删改查实现和专辑功能一样,依样画葫芦即可,这里我们就不再一一演示了。你可以对比 Github 中的源码作为参考:
https://github.com/nonfu/master-laravel-code/tree/v1.2/practice/blog
需要注意的是,学院君没有在源码中提供消息的增加和修改功能,因为消息数据是前台用户提交表单生成的,不是后台生成,后台只需要能够查看和删除即可。
4、小结
好了,关于 PHP 入门到实战系列教程到此就告一段落了,学院君陆续给大家介绍了 PHP 本地开发环境的搭建、代码编辑器的选择、基础语法、函数式编程、面向对象编程、MySQL 数据库操作、HTTP 编程,并且通过一个博客项目进行实战演示,希望通过这个系列的学习,可以帮助你快速入门 PHP 开发。
我们日常使用 PHP 开发 Web 项目通常都是基于框架进行开发的,常见的 PHP Web 框架有 Laravel、Symfony、Yii、ThinkPHP、Phalcon、CakePHP 等,这其中流行度最高的当属 Laravel,作为 PHP 全栈工程师系列最重要的中坚力量,接下来,学院君将给大家介绍这个框架的基本使用,对应课程请点击页面左下角阅读原文链接查看。
PS:本系列 PHP 入门教程和实战项目都已经非常偏向 Laravel 的架构了,所以对你快速入门 Laravel 框架会提供一臂之力。
(全文完)