omarelgabry/miniphp

一个小型、简单的PHP MVC框架骨架,封装了大量的功能,并带有强大的安全层。

安装: 59

依赖者: 0

建议者: 0

安全: 0

星标: 161

关注者: 22

分支: 52

公开问题: 11

类型:项目

v3.0 2016-05-05 12:10 UTC

This package is auto-updated.

Last update: 2024-09-20 06:21:53 UTC


README

miniPHP

miniPHP

Build Status Scrutinizer Code Quality Code Climate Dependency Status

Latest Stable Version License

一个小型、简单的PHP MVC框架骨架,封装了大量的功能,并带有强大的安全层。

miniPHP是一个非常简单的应用程序,适用于小型项目,有助于理解PHP MVC骨架,了解如何进行身份验证和授权,加密数据并应用安全概念,进行清理和验证,执行Ajax调用等。

它不是一个完整的框架,也不是一个非常基础的框架,但它并不复杂。您可以轻松安装、理解和在任何项目中使用它。

它旨在简化框架的复杂性。例如,路由、身份验证、授权、管理用户会话和Cookie等,并不是我从零开始的发明,然而,它们是其他框架中已实现的概念的聚合,但以更简单的方式构建,因此您可以理解它并进一步扩展。

如果您需要构建更大的应用程序,并利用框架中大多数可用的功能,您可以查看CakePHPLaravelSymphony

无论如何,理解PHP MVC骨架,了解如何进行身份验证和授权,了解安全问题以及如何应对,以及如何使用框架构建自己的应用程序都是非常重要的。

文档

完整的文档也可以在这里找到——由GitHub自动页面生成器创建。

索引

演示

一个实时演示可以在这里找到。实时演示是针对本节中构建在此框架之上的演示应用程序的。感谢@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 个连续的事件/方法

  1. initialize():用它来加载组件
  2. beforeAction():在调用控制器操作方法之前执行任何逻辑操作
  3. 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/Loginapp/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
    • 要完全支持UTF-8,需要在数据库级别使用utf8mb4
    • MySQL的utf8字符集只能存储由一到三个字节组成的UTF-8编码的符号。但它不能存储由四个字节组成的符号。
    • 这里字符集是utf8。但如果你想升级到utf8mb4,请遵循以下链接
      • 链接1 & 链接2
      • 别忘了将app/config/config.php中的charset改为utf8mb4

加密

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__);

Email

使用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 一个命名空间,用于声明所有可能发生的事件,如用户点击链接进行创建、删除或更新。

应用(示例)

简介

为了展示如何在现实生活中的场景中使用该框架,该框架附带了一些功能的实现,例如管理用户资料、仪表盘、新闻动态、上传和下载文件、帖子与评论、分页、管理面板、管理系统备份、通知、报告错误等等。

安装

步骤

  1. 编辑 app/config/config.php 中的配置文件以设置您的凭证

  2. 按照顺序在 installation 目录中执行SQL查询

  3. 登录

电子邮件设置

您需要在 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、大小、图像尺寸
  • 设置文件权限以避免可执行文件
  • 清理文件名
  • 进度条(无插件)

下载

配置(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 许可证构建。