在博客后台为专辑、文章、消息模块实现增删改查功能

作为 PHP 博客实战项目的终结篇,我们将在后台管理系统为专辑、文章、消息模块添加增删改查功能,来完成内容生产和消费的闭环。

后台首页重构

在此之前,我们需要先改造后台首页视图,通过博客功能模块替代默认的示例代码。

控制器改造

app/http/controller/admin 目录下新建 AdminController 作为管理后台控制器的基类,并且初始化全局变量:

<?php
namespace App\Http\Controller\Admin;

use App\Http\Controller\Controller;
use App\Model\Message;

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 App\Http\Controller\Admin;

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 让修改代码后引起的自动加载变化生效,重新刷新后台,就可以看到新的后台首页视图了:

-w1430

专辑模块增删改查实现

接下来,我们就可以通过为专辑、文章、消息模块实现增删改查功能,来补全上面侧边栏链接点击后渲染的页面了。

这里我们以专辑为例进行演示。

路由 & 控制器

首先在 app/routes/web.php 中注册对应的路由:

$router->register('get', 'admin/albums', 'Admin\AlbumController@index');
$router->register(['get', 'post'], 'admin/album/new', 'Admin\AlbumController@add');
$router->register(['get', 'post'], 'admin/album/edit', 'Admin\AlbumController@edit');
$router->register(['post'], 'admin/album/delete', 'Admin\AlbumController@delete');

然后在 app/http/controller/admin 目录下创建对应的控制器 AlbumController,以及对应列表页、新增/修改表单、删除处理逻辑:

<?php
namespace App\Http\Controller\Admin;


use App\Http\Exception\ValidationException;
use App\Http\Response;
use App\Model\Album;
use Symfony\Component\HttpFoundation\File\UploadedFile;

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-autonpm run dev 让修改代码生效。

测试专辑增删改查功能

在侧边栏点击专辑列表就可以看到如下渲染的视图效果了:

-w1165

点击侧边栏中的新增专辑链接就可以进入新增专辑页面:

-w1157

在列表页点击编辑按钮,就可以编辑对应的专辑记录:

-w1154

最后,我们可以在专辑列表页通过删除按钮删除对应的专辑,删除前会弹出确认模态框,确认之后就会删除这个专辑:

-w506

其它模块增删改查实现

文章和消息的增删改查实现和专辑功能一样,依样画葫芦即可,这里我们就不再一一演示了。你可以对比 Github 中的源码作为参考:

https://github.com/nonfu/master-laravel-code/tree/v1.2/practice/blog

需要注意的是,学院君没有在源码中提供消息的增加和修改功能,因为消息数据是前台用户提交表单生成的,不是后台生成,后台只需要能够查看和删除即可。

小结

好了,关于 PHP 入门到实战系列教程到此就告一段落了,学院君陆续给大家介绍了 PHP 本地开发环境的搭建、代码编辑器的选择、基础语法、函数式编程、面向对象编程、MySQL 数据库操作、HTTP 编程,并且通过一个博客项目进行实战演示,希望通过这个系列的学习,可以帮助你快速入门 PHP 开发。

我们日常使用 PHP 开发 Web 项目通常都是基于框架进行开发的,常见的 PHP Web 框架有 Laravel、Symfony、Yii、ThinkPHP、Phalcon、CakePHP 等,这其中流行度最高的当属 Laravel,作为 PHP 全栈工程师系列最重要的中坚力量,接下来,学院君将给大家介绍这个框架的基本使用,对应课程入门请点击这里

PS:本系列 PHP 入门教程和实战项目都已经非常偏向 Laravel 的架构了,所以对你快速入门 Laravel 框架会提供一臂之力。

你也可以通过订阅学院君的微信公众号在手机上刷这个系列的完整教程:

学院君订阅号

上一篇: 通过 PHP 原生代码基于 Cookie + Session 机制实现后台用户认证功能

下一篇: 没有下一篇了