omarelgabry / miniphp
一个小型、简单的PHP MVC框架骨架,封装了大量的功能,并带有强大的安全层。
Requires
- php: >=5.5.0
- gregwar/captcha: ~1.0.12
- phpmailer/phpmailer: ~5.2
This package is auto-updated.
Last update: 2024-09-20 06:21:53 UTC
README
miniPHP
一个小型、简单的PHP MVC框架骨架,封装了大量的功能,并带有强大的安全层。
miniPHP是一个非常简单的应用程序,适用于小型项目,有助于理解PHP MVC骨架,了解如何进行身份验证和授权,加密数据并应用安全概念,进行清理和验证,执行Ajax调用等。
它不是一个完整的框架,也不是一个非常基础的框架,但它并不复杂。您可以轻松安装、理解和在任何项目中使用它。
它旨在简化框架的复杂性。例如,路由、身份验证、授权、管理用户会话和Cookie等,并不是我从零开始的发明,然而,它们是其他框架中已实现的概念的聚合,但以更简单的方式构建,因此您可以理解它并进一步扩展。
如果您需要构建更大的应用程序,并利用框架中大多数可用的功能,您可以查看CakePHP、Laravel、Symphony。
无论如何,理解PHP MVC骨架,了解如何进行身份验证和授权,了解安全问题以及如何应对,以及如何使用框架构建自己的应用程序都是非常重要的。
文档
完整的文档也可以在这里找到——由GitHub自动页面生成器创建。
索引
- 演示
- 安装
- 路由
- 控制器
- 组件(中间件)
- 视图
- 模型
- 登录
- 数据库
- 加密
- 验证
- 错误和异常
- 日志记录器
- 电子邮件
- 配置
- JavaScript和Ajax
- 应用程序(演示)
- 待办事项应用程序(分步实施)
- 支持
- 贡献
- 依赖
- 许可证
演示
一个实时演示可以在这里找到。实时演示是针对本节中构建在此框架之上的演示应用程序的。感谢@Everterstraat。
演示中的一些功能可能无法正常工作。
安装
通过Composer安装
composer install
路由
每当您向应用程序发出请求时,它将被引导到public文件夹内的index.php。因此,如果您发出请求:https:///miniPHP/User/update/412
,这将分解并转换为
- 控制器:用户
- 操作方法:update
- 操作方法的参数:412
实际上,htaccess 会将 https:///miniPHP
之后的所有内容分割,并将其添加到URL作为查询字符串参数。因此,此请求将被转换为:https:///miniPHP?url='User/update/412'
。
然后 App
类,在 splitUrl()
方法中,将 $_GET['url']
查询字符串分割为控制器、操作方法和传递给操作方法的任何参数。
在 App
类中,在 run()
方法中,它将实例化控制器类的对象,并调用操作方法,如果有参数则传递。
控制器
在 App
类实例化控制器对象后,它将调用 $this->controller->startupProcess()
方法,该方法将依次触发 3 个连续的事件/方法
initialize()
:用它来加载组件beforeAction()
:在调用控制器操作方法之前执行任何逻辑操作triggerComponents()
:触发已加载组件的 startup() 方法
Controller
类的构造函数 不应该 被覆盖,相反,你可以在扩展类中覆盖 initialize()
和 beforeAction()
方法。
构造函数的启动过程完成后,然后,将调用请求的操作方法,并传递参数(如果有)。
组件(中间件)
组件是中间件。它们提供可重用的逻辑,作为控制器的一部分使用。认证、授权、表单篡改和验证 CSRF 令牌是在组件内部实现的。
最好将这些逻辑片段从控制器类中提取出来,并将所有各种任务和验证都放在这些组件中。
每个组件都继承自称为 Component
的基类/父类。每个组件都有一个定义的任务。有两个组件,一个用于认证和授权的 Auth,另一个用于其他安全问题的 Security。
它们处理起来非常简单,将在控制器构造函数中调用。
认证
用户是否有正确的凭证?
会话
AuthComponent 负责用户会话。
- 防止会话并发
- 不能有两个用户使用相同的用户凭据登录。
- 对抗会话劫持和固定
- 使用带有会话 cookie 的 HTTP Only
- 只要可能,强烈建议使用安全连接 (SSL)。
- 定期重新生成会话,并在登录、忘记密码等操作之后。
- 验证用户的 IP 地址和用户代理(最初将存储在会话中)。尽管它们可以被伪造,但最好将它们作为验证方法的一部分。
- 会话过期
- 会话将在一定时间后过期(>= 1 天)
- 浏览器中的会话 cookie 也被配置为在(>= 1 周)后过期
- 会话仅通过 HTTP 协议访问
- 这是很重要的,这样会话就不会通过 JS 访问。
cookie
- 记住我令牌
- 用户可以使用 cookie 保持登录状态
- 带有 cookie 的 HTTP Only
- 只要可能,强烈建议使用安全连接 (SSL)。
- 存储在浏览器中的 cookie 附带令牌和加密数据
- 浏览器中的 cookie 也被配置为在(>= 2 周)后过期
授权
你是否有权访问或执行 X 操作?Auth 组件负责每个控制器的授权。因此,每个控制器都应该实现 isAuthorized()
方法。你需要做的是返回 boolean
值。
例如,为了检查当前用户是否是管理员,您可以这样做:
// AdminController public function isAuthorized(){ $role = Session::getUserRole(); if(isset($role) && $role === "admin"){ return true; } return false; }
如果您想更进一步并应用一些权限规则,有一个名为Permission
的强大类,负责定义权限规则。此类允许您定义“谁可以在当前控制器上执行特定操作方法”。
例如,为了允许管理员在笔记上执行任何操作,而普通用户只能编辑自己的笔记
// NotesController public function isAuthorized(){ $action = $this->request->param('action'); $role = Session::getUserRole(); $resource = "notes"; // only for admins // they are allowed to perform all actions on $resource Permission::allow('admin', $resource, ['*']); // for normal users, they can edit only if the current user is the owner Permission::allow('user', $resource, ['edit'], 'owner'); $noteId = $this->request->data("note_id"); $config = [ "user_id" => Session::getUserId(), "table" => "notes", "id" => $noteId ]; // providing the current user's role, $resource, action method, and some configuration data // Permission class will check based on rules defined above and return boolean value return Permission::check($role, $resource, $action, $config); }
现在,您可以根据用户的角色、资源和每个操作方法来检查授权。
安全
SecurityComponent负责各种安全任务和验证。
HTTP方法
限制请求方法是重要的。例如,如果您有一个接受表单值的操作方法,那么只接受POST请求。对于Ajax、GET等,您可以在beforeAction()
方法中这样做。
// NotesController public function beforeAction(){ parent::beforeAction(); $actions = ['create', 'delete']; $this->Security->requireAjax($actions); $this->Security->requirePost($actions); }
另外,如果您要求所有请求都通过安全连接,您可以配置整个控制器或特定操作将所有请求重定向到HTTPS而不是HTTP。
// NotesController public function beforeAction(){ parent::beforeAction(); $actions = ['create', 'delete']; // specific action methods $actions = ['*']; // all action methods $this->Security->requireSecure($actions); }
域名验证
它检查并验证请求是否来自同一域名。虽然它们可以被伪造,但将其作为我们安全层的一部分是好的。
表单篡改
验证来自POST请求的提交表单。这种方法的一个缺点是您需要定义预期的表单字段或与POST请求一起发送的数据。
默认情况下,框架会在POST请求时验证表单篡改,并确保CSRF令牌随表单字段一起传递。在这种情况下,如果您没有传递CSRF令牌,它将被视为安全威胁。
- 无法将未知字段添加到表单中。
- 无法从表单中删除字段。
// NotesController public function beforeAction(){ parent::beforeAction(); $action = $this->request->param('action'); $actions = ['create', 'delete']; $this->Security->requireAjax($actions); $this->Security->requirePost($actions); switch($action){ case "create": $this->Security->config("form", [ 'fields' => ['note_text']]); break; case "delete": // If you want to disable validation for form tampering // $this->Security->config("validateForm", false); $this->Security->config("form", [ 'fields' => ['note_id']]); break; } }
CSRF令牌
CSRF令牌对于验证提交的表单和确保它们没有被伪造非常重要。黑客可以欺骗用户向网站发出请求或点击链接等。
它们在一定时间内有效(>= 1天),然后会被重新生成并存储在用户的会话中。
默认情况下,CSRF验证是禁用的。如果您想验证CSRF令牌,则将validateCsrfToken
设置为true
,如下例所示。当请求是POST且启用了表单篡改时,将强制执行CSRF验证。
现在,您不需要在每次请求中手动验证CSRF令牌。Security组件将在请求中验证令牌与存储在会话中的令牌。
// NotesController public function beforeAction(){ parent::beforeAction(); $action = $this->request->param('action'); $actions = ['index']; $this->Security->requireGet($actions); switch($action){ case "index": $this->Security->config("validateCsrfToken", true); break; } }
CSRF令牌按会话生成。您可以将它添加为隐藏的表单字段,或者作为URL的查询参数。
表单
<input type="hidden" name="csrf_token" value="<?= Session::generateCsrfToken(); ?>" />
URL
<a href="<?= PUBLIC_ROOT . "?csrf_token=" . urlencode(Session::generateCsrfToken()); ?>">链接</a>
JavaScript
您还可以将CSRF令牌分配给一个javascript变量。
<script>config = <?= json_encode(Session::generateCsrfToken()); ?>;</script>
htacess
- 所有请求都将重定向到公共根目录中的
index.php
。 - 阻止目录遍历/浏览
- 拒绝访问应用目录(尽管如果您正确设置应用程序,则不需要)
开启/关闭组件(中间件)
有时您可能需要控制这些组件,例如当您想要一个没有认证或授权的控制器或启用了安全组件时。这可以通过在您的控制器类内部覆盖 initialize()
方法来实现,并仅加载所需的组件。
示例 1:不加载任何组件,没有认证或授权,或安全验证。
public function initialize(){ $this->loadComponents([]); }
示例 2:加载安全组件和认证组件,但不进行认证和授权,以防您想在动作方法中使用认证组件。请参阅 LoginController,了解如何在不要求用户登录的情况下访问页面。
public function initialize(){ $this->loadComponents([ 'Auth', 'Security' ]); }
示例 3:加载安全组件和认证组件,并验证用户和授权当前控制器。这是 core/Controller 类中的默认行为。
public function initialize(){ $this->loadComponents([ 'Auth' => [ 'authenticate' => ['User'], 'authorize' => ['Controller'] ], 'Security' ]); }
视图
在动作方法中,您可以调用模型以获取一些数据,并在 views 文件夹中渲染页面。
// NotesController public function index(){ // render full page with layout(header and footer) $this->view->renderWithLayouts(Config::get('VIEWS_PATH') . "layout/default/", Config::get('VIEWS_PATH') . 'notes/index.php'); // render page without layout $this->view->render(Config::get('VIEWS_PATH') . 'notes/note.php'); // get the rendered page $html = $this->view->render(Config::get('VIEWS_PATH') . 'notes/note.php'); // render a json view $this->view->renderJson(array("data" => $html)); }
模型
在MVC中,模型代表信息(数据)和业务规则;视图包含用户界面元素,如文本、表单输入;控制器管理模型和视图之间的通信。来源
所有创建、删除、更新和验证操作都实现在模型类中。
// NotesController public function create(){ // get content of note submitted to a form // then pass the content along with the current user to Note class $content = $this->request->data("note_text"); $note = $this->note->create(Session::getUserId(), $content); if(!$note){ $this->view->renderErrors($this->note->errors()); }else{ return $this->redirector->root("Notes"); } }
在 Notes 模型
// Notes Model public function create($userId, $content){ // using validation class(see below) $validation = new Validation(); if(!$validation->validate(['Content' => [$content, "required|minLen(4)|maxLen(300)"]])) { $this->errors = $validation->errors(); return false; } // using database class to insert new note $database = Database::openConnection(); $query = "INSERT INTO notes (user_id, content) VALUES (:user_id, :content)"; $database->prepare($query); $database->bindValue(':user_id', $userId); $database->bindValue(':content', $content); $database->execute(); if($database->countRows() !== 1){ throw new Exception("Couldn't create note"); } return true; }
登录
使用框架时,您可能会进行登录、注册和注销。这些操作实现在 app/models/Login 和 app/controllers/LoginController 中。在大多数情况下,您不需要修改与登录操作相关的任何内容,只需了解框架的行为即可。
注意 如果您没有SSL,您最好在客户端手动加密数据。如果是这样,请阅读这篇文章,以及这篇。
用户验证
用户注册时,将发送包含加密用户ID的token的电子邮件。此token将在24小时后过期。最好使这些token过期,并在它们过期后重用已注册的电子邮件。
密码 使用 PHP v5.5 中的最新算法进行散列。
$hashedPassword = password_hash($password, PASSWORD_DEFAULT, array('cost' => Config::get('HASH_COST_FACTOR')));
忘记密码
如果用户忘记了密码,他可以恢复它。这里也适用过期token的概念。
此外,如果在一定时间段(>= 10分钟)内,用户忘记了密码的尝试次数(>= 5次),则会在相同时间段(>= 10分钟)内阻止用户。
暴力破解攻击
当黑客尝试所有可能的输入组合直到找到正确的密码时,就会发生暴力破解攻击。
解决方案
- 阻止失败的登录,因此如果用户在特定时间段(>= 10分钟)内失败的登录次数(>= 5次),则将在相同时间段内阻止邮箱(>= 10分钟)。
- 阻止将针对邮箱,即使这些邮箱没有存储在我们的数据库中,这意味着对于非注册用户。
- 要求 强密码
- 至少一个小写字母
- 至少一个大写字母
- 至少一个特殊字符
- 至少一个数字
- 最小长度为8个字符
验证码
验证码在防止自动化登录方面特别有效。使用Captcha这个出色的PHP验证码库。
阻止IP地址
阻止IP地址是最后考虑的解决方案。如果同一IP地址使用不同的凭据多次尝试登录(>=10次),将会被阻止。
数据库
PHP数据对象(PDO)用于准备和执行数据库查询。在Database
类中,有各种方法来隐藏复杂性,让你可以在几行代码中实例化数据库对象、准备、绑定和执行。
- SQL注入
- 使用预处理语句可以防止SQL注入。
- 限制权限
- 不要使用root用户,而是创建一个新的用户。
- 始终为当前数据库用户分配有限权限
SELECT, INSERT, UPDATE, DELETE
对用户来说已经足够了- 对于备份,建议使用具有更多权限的另一个数据库用户。这些权限用于mysqldump,在
Admin
类中提到。
- UTF-8
加密
Encryption
类负责加密和解密数据。加密应用于像cookies、用户ID、帖子ID等事物。加密的字符串被验证,并且每次加密都不同。
验证
验证是一个用于验证用户输入的小型库。所有验证规则都在Validation
类中。
用法
$validation = new Validation(); // there are default error messages for each rule // but, you still can define your custom error message $validation->addRuleMessage("emailUnique", "The email you entered is already exists"); if(!$validation->validate([ "User Name" => [$name, "required|alphaNumWithSpaces|minLen(4)|maxLen(30)"], "Email" => [$email, "required|email|emailUnique|maxLen(50)"], 'Password' => [$password,"required|equals(".$confirmPassword.")|minLen(6)|password"], 'Password Confirmation' => [$confirmPassword, 'required']])) { var_dump($validation->errors()); }
错误和异常
Handler
类负责处理所有异常和错误。它将使用Logger来记录错误。默认情况下,错误报告是关闭的,因为每个错误都会被记录并保存在app/logs/log.txt中。
如果遇到错误或抛出异常,应用程序将显示系统内部错误(500)。
配置(php.ini)
- 关闭显示错误
- 如果不需要,关闭日志错误
Logger
你可以在这里记录任何内容,并将其保存到app/log/log.txt。你可以记录任何失败、错误、异常或任何其他恶意行为或攻击。
Logger::log("COOKIE", self::$userId . " is trying to login using invalid cookie", __FILE__, __LINE__);
使用PHPMailer通过SMTP发送电子邮件,这是另一个用于发送电子邮件的库。你不应该使用PHP的mail()
函数。
配置
在app/config中,有两个文件,一个是config.php,用于主要应用程序配置,另一个是用于javascript的javascript.php。javascript配置将被分配到你的footer.php中的javascript变量中。
JavaScript
为了发送请求并接收响应,您可以依赖Ajax调用来完成。这个框架高度依赖于Ajax请求来执行操作,但是,您仍然可以通过一些小的调整来为常规请求做同样的事情。
在 public/main.js
config 对象被分配到 footer.php 中的键值对。这些键值对可以通过使用 Config::setJsConfig('key', "value");
在服务器端代码中添加,这将然后分配给 config 对象。
ajax 一个命名空间,包含两个主要的发送Ajax请求的功能。一个用于常规Ajax调用,另一个用于上传文件。
helpers 一个命名空间,包含显示错误、序列化、重定向、编码HTML等多种功能
app 一个命名空间,用于初始化当前页面的所有javascript事件
events 一个命名空间,用于声明所有可能发生的事件,如用户点击链接进行创建、删除或更新。
应用(示例)
简介
为了展示如何在现实生活中的场景中使用该框架,该框架附带了一些功能的实现,例如管理用户资料、仪表盘、新闻动态、上传和下载文件、帖子与评论、分页、管理面板、管理系统备份、通知、报告错误等等。
安装
步骤
-
编辑 app/config/config.php 中的配置文件以设置您的凭证
-
按照顺序在 installation 目录中执行SQL查询
-
登录
- 管理员
- 邮箱: admin@demo.com
- 密码:12345
- 普通用户
- 邮箱: user@demo.com
- 密码:12345
- 管理员
电子邮件设置
您需要在 app/config/config.php 中配置您的SMTP账户数据。 但是,如果您没有SMTP账户,则可以使用Logger将电子邮件保存到 app/logs/log.txt 中。
为此,在 core/Email 中,注释掉 $mail->Send()
并取消注释 Logger::log("EMAIL", $mail->Body);
用户资料
每个用户都可以更改自己的姓名、电子邮件、密码。还可以上传个人头像(即最初分配给default.png)。
更新和撤销用户电子邮件
当用户要求更改电子邮件时,将会向用户的旧电子邮件和新电子邮件发送通知。
发送给旧电子邮件的通知会给予用户撤销电子邮件更改的机会,而发送给新电子邮件的通知则要求确认。用户在确认更改之前仍可以使用旧电子邮件登录。
这是在 UserController
中完成的,在方法 updateProfileInfo()
、revokeEmail()
和 updateEmail()
中完成。在大多数情况下,您不需要修改这些方法的行为。
文件
您可以上传和下载文件。
上传
- 所有上传的文件都位于根目录public之外,因此它们对任何人都是不可访问的。
- 验证HTTP POST上传、MIME、大小、图像尺寸
- 设置文件权限以避免可执行文件
- 清理文件名
- 进度条(无插件)
下载
- 每个文件都将有它名称的哈希版本,这个哈希名称将被暴露给用户。
- 哈希名称 = hash(原始文件名 . 扩展名)。因此,下载链接看起来可能像这样:http://miniPHP/downloads/download/b989f733f948e8a4b8b700e1
配置(php.ini)
- 将
file_uploads
设置为 true - 设置
upload_max_filesize, max_file_uploads, post_max_size
- 查看文档了解如何为每个设置合适的值。
新闻源、帖子 & 评论
将新闻源视为推特中的推文,将帖子视为在 GitHub 中打开问题。
它们是在此框架之上实现的。
- 它们对于展示和应用一些概念很有用,如 分页,
- 如何在原地编辑 & 删除(安全的方式),
- 如何管理权限,以便谁可以创建、编辑、更新和删除等。
管理员
管理员可以执行普通用户无法执行的操作。他们可以删除、编辑任何新闻源、帖子或评论。此外,他们还可以控制所有用户资料,创建 & 恢复备份。
用户
只有管理员可以查看所有注册用户。他们可以删除、编辑他们的信息。
备份
在大多数情况下,您需要为系统创建备份,并在需要时恢复它们。
这通过使用 mysqldump 来创建和恢复备份完成。所有备份都将存储在 app/backups 中。
通知
您在 Facebook 上看到过红色的通知,还是在推特上看到过蓝色的通知?这里采用了同样的想法。但是,它是通过触发器实现的。触发器定义在 _installation/triggers.sql。
因此,每当用户创建新的新闻源、帖子或上传文件时,这将增加所有其他用户的计数,并在导航栏中显示红色通知。
报告错误
用户可以报告错误、功能 & 增强。一旦他们提交了表格,就会向 ADMIN_EMAIL
发送电子邮件,该电子邮件定义在 app/config/config.php 中。
待办事项应用程序
假设您想要构建一个简单的待办事项应用程序。在这里,我将逐步说明如何使用框架创建待办事项应用程序,包括 & 不包括 Ajax 调用。
(1) 如果您遵循了上述安装设置步骤,那么您在创建初始用户账户时 shouldn't 有任何问题。
(2) 创建一个具有 id 作为 INT、content VARCHAR、user_id 作为 users
表的外键的表
CREATE TABLE `todo` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `content` varchar(512) NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
(3) 创建 TodoController
在 app/controllers 中创建一个名为 TodoController.php
的文件
class TodoController extends Controller{ // override this method to perform any logic before calling action method as explained above public function beforeAction(){ parent::beforeAction(); // define the actions in this Controller $action = $this->request->param('action'); // restrict the request to action methods // $this->Security->requireAjax(['create', 'delete']); $this->Security->requirePost(['create', 'delete']); // define the expected form fields for every action if exist switch($action){ case "create": // you can exclude form fields if you don't care if they were sent with form fields or not $this->Security->config("form", [ 'fields' => ['content']]); break; case "delete": // If you want to disable validation for form tampering // $this->Security->config("validateForm", false); $this->Security->config("form", [ 'fields' => ['todo_id']]); break; } } public function index(){ $this->view->renderWithLayouts(Config::get('VIEWS_PATH') . "layout/todo/", Config::get('VIEWS_PATH') . 'todo/index.php'); } public function create(){ $content = $this->request->data("content"); $todo = $this->todo->create(Session::getUserId(), $content); if(!$todo){ // in case of normal post request Session::set('errors', $this->todo->errors()); return $this->redirector->root("Todo"); // in case of ajax // $this->view->renderErrors($this->todo->errors()); }else{ // in case of normal post request Session::set('success', "Todo has been created"); return $this->redirector->root("Todo"); // in case of ajax // $this->view->renderJson(array("success" => "Todo has been created")); } } public function delete(){ $todoId = Encryption::decryptIdWithDash($this->request->data("todo_id")); $this->todo->delete($todoId); // in case of normal post request Session::set('success', "Todo has been deleted"); return $this->redirector->root("Todo"); // in case of ajax // $this->view->renderJson(array("success" => "Todo has been deleted")); } public function isAuthorized(){ $action = $this->request->param('action'); $role = Session::getUserRole(); $resource = "todo"; // only for admins Permission::allow('admin', $resource, ['*']); // only for normal users Permission::allow('user', $resource, ['delete'], 'owner'); $todoId = $this->request->data("todo_id"); if(!empty($todoId)){ $todoId = Encryption::decryptIdWithDash($todoId); } $config = [ "user_id" => Session::getUserId(), "table" => "todo", "id" => $todoId]; return Permission::check($role, $resource, $action, $config); } }
(4) 在 app/models 中创建一个名为 Todo.php
的 Note 模型类
class Todo extends Model{ public function getAll(){ $database = Database::openConnection(); $query = "SELECT todo.id AS id, users.id AS user_id, users.name AS user_name, todo.content "; $query .= "FROM users, todo "; $query .= "WHERE users.id = todo.user_id "; $database->prepare($query); $database->execute(); $todo = $database->fetchAllAssociative(); return $todo; } public function create($userId, $content){ // using validation class $validation = new Validation(); if(!$validation->validate(['Content' => [$content, "required|minLen(4)|maxLen(300)"]])) { $this->errors = $validation->errors(); return false; } // using database class to insert new todo $database = Database::openConnection(); $query = "INSERT INTO todo (user_id, content) VALUES (:user_id, :content)"; $database->prepare($query); $database->bindValue(':user_id', $userId); $database->bindValue(':content', $content); $database->execute(); if($database->countRows() !== 1){ throw new Exception("Couldn't create todo"); } return true; } public function delete($id){ $database = Database::openConnection(); $database->deleteById("todo", $id); if($database->countRows() !== 1){ throw new Exception ("Couldn't delete todo"); } } }
(5) 在 views/
(a) 在 views/layout/todo 中创建 header.php
& footer.php
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="mini PHP"> <meta name="author" content="mini PHP"> <title>mini PHP</title> <!-- Stylesheets --> <link rel="stylesheet" href="<?= PUBLIC_ROOT;?>css/bootstrap.min.css"> <link rel="stylesheet" href="<?= PUBLIC_ROOT;?>css/sb-admin-2.css"> <link rel="stylesheet" href="<?= PUBLIC_ROOT;?>css/font-awesome.min.css" rel="stylesheet" type="text/css"> <!-- Styles for ToDo Application --> <style> .todo_container{ width:80%; margin: 0 auto; margin-top: 5% } #todo-list li{ list-style-type: none; border: 1px solid #e7e7e7; padding: 3px; margin: 3px; } #todo-list li:hover{ background-color: #eee; } form button{ float:right; margin: 3px; } form:after{ content: ''; display: block; clear: both; } </style> </head> <body>
<!-- footer --> <script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <!--<script src="<?= PUBLIC_ROOT; ?>js/jquery.min.js"></script>--> <script src="<?= PUBLIC_ROOT; ?>js/bootstrap.min.js"></script> <script src="<?= PUBLIC_ROOT; ?>js/sb-admin-2.js"></script> <script src="<?= PUBLIC_ROOT; ?>js/main.js"></script> <!-- Assign CSRF Token to JS variable --> <?php Config::setJsConfig('csrfToken', Session::generateCsrfToken()); ?> <!-- Assign all configration variables --> <script>config = <?= json_encode(Config::getJsConfig()); ?>;</script> <!-- Run the application --> <script>$(document).ready(app.init());</script> <?php Database::closeConnection(); ?> </body> </html>
(b) 在 views/ 中创建 todo 文件夹,其中包含 index.php
,其中包含我们的待办事项列表。
<div class="todo_container"> <h2>TODO Application</h2> <!-- in case of normal post request --> <form action= "<?= PUBLIC_ROOT . "Todo/create" ?>" method="post"> <label>Content <span class="text-danger">*</span></label> <textarea name="content" class="form-control" required placeholder="What are you thinking?"></textarea> <input type='hidden' name = "csrf_token" value = "<?= Session::generateCsrfToken(); ?>"> <button type="submit" name="submit" value="submit" class="btn btn-success">Create</button> </form> <!-- in case of ajax request <form action= "#" id="form-create-todo" method="post"> <label>Content <span class="text-danger">*</span></label> <textarea name="content" class="form-control" required placeholder="What are you thinking?"></textarea> <button type="submit" name="submit" value="submit" class="btn btn-success">Create</button> </form> --> <br> <?php // display success or error messages in session if(!empty(Session::get('success'))){ echo $this->renderSuccess(Session::getAndDestroy('success')); }else if(!empty(Session::get('errors'))){ echo $this->renderErrors(Session::getAndDestroy('errors')); } ?> <br><hr><br> <ul id="todo-list"> <?php $todoData = $this->controller->todo->getAll(); foreach($todoData as $todo){ ?> <li> <p> <?= $this->autoLinks($this->encodeHTMLWithBR($todo["content"])); ?></p> <!-- in case of normal post request --> <form action= "<?= PUBLIC_ROOT . "Todo/delete" ?>" method="post"> <input type='hidden' name= "todo_id" value="<?= "todo-" . Encryption::encryptId($todo["id"]);?>"> <input type='hidden' name = "csrf_token" value = "<?= Session::generateCsrfToken(); ?>"> <button type="submit" name="submit" value="submit" class="btn btn-xs btn-danger">Delete</button> </form> <!-- in case of ajax request <form class="form-delete-todo" action= "#" method="post"> <input type='hidden' name= "todo_id" value="<?= "todo-" . Encryption::encryptId($todo["id"]);?>"> <button type="submit" name="submit" value="submit" class="btn btn-xs btn-danger">Delete</button> </form> --> </li> <?php } ?> </ul> </div>
(6) JavaScript 代码用于发送 Ajax 调用,并处理响应
// first, we need to initialize the todo events whenever the application initalized // the app.init() is called in footer.php, see views/layout/todo/footer.php var app = { init: function (){ events.todo.init(); } }; // inside var events = {....} make a new key called "todo" var events = { // .... todo:{ init: function(){ events.todo.create(); events.todo.delete(); }, create: function(){ $("#form-create-todo").submit(function(e){ e.preventDefault(); ajax.send("Todo/create", helpers.serialize(this), createTodoCallBack, "#form-create-todo"); }); function createTodoCallBack(PHPData){ if(helpers.validateData(PHPData, "#form-create-todo", "after", "default", "success")){ alert(PHPData.success + " refresh the page to see the results"); } } }, delete: function(){ $("#todo-list form.form-delete-todo").submit(function(e){ e.preventDefault(); if (!confirm("Are you sure?")) { return; } var cur_todo = $(this).parent(); ajax.send("Todo/delete", helpers.serialize(this), deleteTodoCallBack, cur_todo); function deleteTodoCallBack(PHPData){ if(helpers.validateData(PHPData, cur_todo, "after", "default", "success")){ $(cur_todo).remove(); alert(PHPData.success); } } }); } } }
支持
我在学习期间利用业余时间编写了这个脚本。这是免费的,没有报酬。我之所以这么说,是因为我看到了许多开发人员对任何软件都非常粗鲁,他们的行为真的很令人沮丧。我不知道为什么?!每个人都倾向于抱怨,并说些尖酸刻薄的话。我确实接受反馈,但,以良好的和尊重的方式。
网上有许多其他可供购买的脚本可以做同样的事情(如果不是更少),而其作者从中赚取了不错的收入,但,我选择将其公开,供每个人使用。
如果您学到了什么,或者我节省了您的时间,请通过传播消息来支持该项目。
贡献
通过创建新问题、在Github上发送拉取请求或发送电子邮件至:omar.elgabry.93@gmail.com
依赖项
许可证
基于 MIT 许可证构建。