在博客后台为内容模块实现增删改查功能

2020-10-19 09:44:21 浏览数 (1)

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

1、后台首页重构

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

控制器改造

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

代码语言:javascript复制
<?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 继承自这个基类:

代码语言:javascript复制
<?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 部分视图模板代码如下:

代码语言:javascript复制
<!-- 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 部分视图模板代码如下:

代码语言:javascript复制
<!-- 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 中注册对应的路由:

代码语言:javascript复制
$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,以及对应列表页、新增/修改表单、删除处理逻辑:

代码语言:javascript复制
<?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

代码语言:javascript复制
<?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 下,用于渲染列表页分页组件:

代码语言:javascript复制
<!--分页-->
<?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

代码语言:javascript复制
<?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

代码语言:javascript复制
<?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 代码:

代码语言:javascript复制
<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 中:

代码语言:javascript复制
<!-- 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 请求代码完成专辑删除功能:

代码语言:javascript复制
$(function () {
    $('.btn-delete').on('click', function () {
        $('#deleteItemId').val($(this).attr('data-id'));
    })
    $('#deleteItemBtn').on('click', function () {
        $('#deleteItemForm').submit();
    });
});

运行 composer dump-autonpm 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 框架会提供一臂之力。

(全文完)

0 人点赞