Apache路由重写: .htaccess
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [L]
db.php
<?php
$pdo = new PDO(
'mysql:host=localhost;dbname=test_db',
'root',
'',
[PDO::ATTR_EMULATE_PREPARES=>false]);
return $pdo;
MyHttpException.php
<?php
/**
* 自定义异常
* Class MyHttpException
*/
class MyHttpException extends Exception
{
private $statusCode;// code状态码
// $statusCode : code状态码
public function __construct($statusCode, $message = '', $code = 0, $exception = null)
{
parent::__construct($message, $code, $exception);
$this->statusCode = $statusCode;
}
// 得到状态码
public function getStatusCode()
{
return $this->statusCode;
}
}
Article.php
<?php
/**
* 文章 Article.php
*/
require_once __DIR__ . '/MyHttpException.php';
class Article
{
private $db = null;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* 发表文章
* @param string $title
* @param string $content
* @param integer $userId
* @return array
* @throws Exception
*/
public function create($title, $content, $userId)
{
if (empty($title))
{
throw new MyHttpException(422, '标题不能为空');
}
if (empty($content))
{
throw new MyHttpException(422, '内容不能为空');
}
if (empty($userId))
{
throw new MyHttpException(422, '用户ID不能为空');
}
$sql = 'INSERT INTO `article` (`title`,`createdAt`,`content`,`userId`) VALUES (:title,:createdAt,:content,:userId)';
$createdAt = time();
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':title', $title);
$stmt->bindParam(':content', $content);
$stmt->bindParam(':userId', $userId);
$stmt->bindParam(':createdAt', $createdAt);
if (!$stmt->execute())
{
throw new MyHttpException(500, '发表失败');
}
return [
'articleId' => $this->db->lastInsertId(),
'title' => $title,
'content' => $content,
'createdAt' => $createdAt,
'userId' => $userId
];
}
/**
* 查看文章
* @param integer $articleId
* @return mixed
* @throws Exception
*/
public function view($articleId)
{
$sql = 'SELECT * FROM `article` WHERE `articleId`=:id';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':id', $articleId, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($data))
{
throw new MyHttpException(404, '文章不存在');
}
return $data;
}
/**
* 编辑文章
* @param integer $articleId
* @param string $title
* @param string $content
* @param integer $userId
* @return array
* @throws Exception
*/
public function update($articleId, $title, $content, $userId)
{
$article = $this->view($articleId);
if ($article['userId'] != $userId)
{
throw new MyHttpException(403, '你没有权限修改该文章');
}
$sql = 'UPDATE `article` SET `title`=:title,`content`=:content WHERE articleId=:id';
$stmt = $this->db->prepare($sql);
$t = empty($title) ? $article['title'] : $title;
$stmt->bindParam(':title', $t);
$c = empty($content) ? $article['content'] : $content;
$stmt->bindParam(':content',$c);
$stmt->bindParam(':id', $articleId);
if (!$stmt->execute())
{
throw new MyHttpException(500, '编辑失败');
}
return [
'articleId' => $articleId,
'title' => $t,
'content' => $c,
'createdAt' => $article['createdAt'],
'userId' => $userId
];
}
/**
* 文章列表
* @param string $userId
* @param int $page
* @param int $limit
* @return array
*/
public function all($userId, $page = 1, $limit = 10)
{
$sql = 'SELECT * FROM `article` WHERE `userId`=:userId ORDER BY `articleId` DESC LIMIT :offset,:limit';
$offset = ($page - 1) * $limit;
if ($offset < 0)
{
$offset = 0;
}
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':userId', $userId, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 删除文章
* @param string $articleId
* @param integer $userId
* @throws Exception
*/
public function delete($articleId, $userId)
{
$article = $this->view($articleId);
if ($article['userId'] != $userId)
{
throw new MyHttpException(404, '文章不存在');
}
$sql = 'DELETE FROM `article` WHERE `articleId`=:articleId AND `userId`=:userId';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':articleId', $articleId);
$stmt->bindParam(':userId', $userId);
if (!$stmt->execute())
{
throw new MyHttpException(500, '删除失败');
}
}
}
User.php
<?php
/**
* Project: imooc-1
* User: xialeistudio
* Date: 2016/9/16 0016
* Time: 22:10
*/
require_once __DIR__ . '/MyHttpException.php';
class User
{
/**
* @var PDO
*/
private $db = null;
/**
* @var string MD5加密混淆
*/
private $salt = 'imooc';
/**
* User constructor.
* @param PDO $db
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* 注册
* @param string $username
* @param string $password
* @return array
* @throws Exception
*/
public function register($username, $password)
{
$username = trim($username);
if (empty($username))
{
throw new MyHttpException(422, '用户名不能为空');
}
$password = trim($password);
if (empty($password))
{
throw new MyHttpException(422, '密码不能为空');
}
//检测是否存在该用户
if ($this->isUsernameExists($username))
{
throw new MyHttpException(422, '用户名已存在');
}
$password = md5($password . $this->salt);
$createdAt = time();
if ($this->db === null)
{
throw new MyHttpException(500, '数据库连接失败');
}
$sql = 'INSERT INTO `user`(`username`,`password`,`createdAt`) VALUES(:username,:password,:createdAt)';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
$stmt->bindParam(':createdAt', $createdAt);
if (!$stmt->execute())
{
throw new MyHttpException(500, '注册失败');
}
$userId = $this->db->lastInsertId();
return [
'userId' => $userId,
'username' => $username,
'createdAt' => $createdAt
];
}
/**
* 登录
* @param string $username
* @param string $password
* @return array
* @throws Exception
*/
public function login($username, $password)
{
$sql = 'SELECT * FROM `user` WHERE `username`=:username';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':username', $username);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($user))
{
throw new MyHttpException(422, '用户名或密码错误');
}
if ($user['password'] != md5($password . $this->salt))
{
throw new MyHttpException(422, '用户名或密码错误');
}
//TOOD:使用授权表
unset($user['password']);
return $user;
}
/**
* 检测用户名是否存在
* @param $username
* @return bool
* @throws Exception
*/
private function isUsernameExists($username)
{
if ($this->db === null)
{
throw new MyHttpException(500, '数据库连接失败');
}
$sql = 'SELECT userId FROM `user` WHERE username = :username';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':username', $username);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return !empty($data);
}
/**
* 查看用户
* @param $userId
* @return mixed
* @throws MyHttpException
*/
public function view($userId)
{
$sql = 'SELECT * FROM `user` WHERE userId=:id';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':id', $userId, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($data))
{
throw new MyHttpException(404, '用户不存在');
}
return $data;
}
/**
* 编辑
* @param $userId
* @param $password
* @return mixed
* @throws MyHttpException
*/
public function update($userId, $password)
{
$user = $this->view($userId);
if (empty($password))
{
throw new MyHttpException(422, '密码不能为空');
}
$sql = 'UPDATE `user` SET `password` = :password WHERE userId=:id';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':id', $userId);
$password = md5($password . $this->salt);
$stmt->bindParam(':password', $password);
if (!$stmt->execute())
{
throw new MyHttpException(500, '编辑失败');
}
return $user;
}
}
restful.php
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
CREATE SCHEMA IF NOT EXISTS `restful` DEFAULT CHARACTER SET utf8mb4 ;
USE `restful` ;
DROP TABLE IF EXISTS `restful`.`user` ;
CREATE TABLE IF NOT EXISTS `restful`.`user` (
`userId` INT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(20) NOT NULL COMMENT '用户名',
`password` CHAR(32) NOT NULL COMMENT '密码',
`createdAt` INT NOT NULL COMMENT '注册时间',
PRIMARY KEY (`userId`),
UNIQUE INDEX `username` (`username` ASC),
INDEX `createdAt` (`createdAt` ASC))
ENGINE = InnoDB;
DROP TABLE IF EXISTS `restful`.`article` ;
CREATE TABLE IF NOT EXISTS `restful`.`article` (
`articleId` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文章ID',
`title` VARCHAR(40) NOT NULL COMMENT '标题',
`createdAt` INT NOT NULL COMMENT '发表时间',
`content` TEXT NOT NULL COMMENT '文章内容',
`userId` INT NOT NULL COMMENT '用户ID',
PRIMARY KEY (`articleId`),
INDEX `title` (`title` ASC),
INDEX `createdAt` (`createdAt` ASC),
INDEX `fk_article_user_idx` (`userId` ASC),
CONSTRAINT `fk_article_user`
FOREIGN KEY (`userId`)
REFERENCES `restful`.`user` (`userId`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
DROP TABLE IF EXISTS `restful`.`session` ;
CREATE TABLE IF NOT EXISTS `restful`.`session` (
`token` CHAR(8) NOT NULL COMMENT '会话token',
`createdAt` INT NOT NULL COMMENT '登录时间',
`expiresAt` INT NOT NULL COMMENT '过期时间',
`userId` INT NOT NULL COMMENT '用户ID',
PRIMARY KEY (`token`),
INDEX `fk_session_user1_idx` (`userId` ASC),
INDEX `createdAt` (`createdAt` ASC),
INDEX `expires` (`expiresAt` ASC),
CONSTRAINT `fk_session_user1`
FOREIGN KEY (`userId`)
REFERENCES `restful`.`user` (`userId`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
index.php
<?php
require_once __DIR__ . '/../handler/User.php';
require_once __DIR__ . '/../handler/Article.php';
require_once __DIR__ . '/../handler/MyHttpException.php';
class Bridge
{
private $user;// User
private $article; // Article
private $method;// 请求方法
private $entity;// 请求实体
private $id; // 资源id
private $allowEntities = ['users', 'articles'];
private $allowMethods = ['GET', 'POST', 'PUT', 'OPTIONS', 'HEAD', 'DELETE'];
public function __construct()
{
$pdo = require_once __DIR__ . '/../db.php';
$this->user = new User($pdo);
$this->article = new Article($pdo);
$this->setupRequestMethod();
$this->setupEntity();
}
/**
* 初始化实体
*/
private function setupEntity()
{
$pathinfo = $_SERVER['PATH_INFO'];
if (empty($pathinfo))
{
static::sendHttpStatus(400);
}
$params = explode('/', $pathinfo);
$this->entity = $params[1];
if (!in_array($this->entity, $this->allowEntities))
{
//实体不存在
static::sendHttpStatus(404);
}
if (!empty($params[2]))
{
$this->id = $params[2];
}
}
/**
* 发送HTTP响应状态码
* @param $code
* @param string $error
* @param array|null $data
* @internal param null $message
*/
static function sendHttpStatus($code, $error = '', $data = null)
{
static $_status = array(
// Informational 1xx
100 => 'Continue',
101 => 'Switching Protocols',
// Success 2xx
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
// Redirection 3xx
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found', // 1.1
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
// 306 is deprecated but reserved
307 => 'Temporary Redirect',
// Client Error 4xx
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
// Server Error 5xx
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
509 => 'Bandwidth Limit Exceeded'
);
if (isset($_status[$code]))
{
// HTTP/1.1 405 Method Not Allowed
header('HTTP/1.1 ' . $code . ' ' . $_status[$code]);
header('Content-Type:application/json;charset=utf-8');
if ($code == 200) //2xx状态码
{
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
else if ($code == 204)
{
//无响应体
}
else
{
if (empty($error))
{
$error = $_status[$code];
}
echo json_encode(['error' => $error], JSON_UNESCAPED_UNICODE);
}
}
exit(0);
}
/**
* 初始化请求方法
*/
private function setupRequestMethod()
{
$this->method = $_SERVER['REQUEST_METHOD'];
if (!in_array($this->method, $this->allowMethods))
{
//请求方法不被允许
static::sendHttpStatus(405);
}
}
/**
* 处理请求
*/
public function handle()
{
try
{
if ($this->entity == 'users')
{
$this->handleUser();
}
if ($this->entity == 'articles')
{
$this->handleArticle();
}
}
catch (MyHttpException $e)
{
static::sendHttpStatus($e->getStatusCode(), $e->getMessage());
}
}
/**
* 获取请求体
* @return mixed|null
*/
private function getBodyParams()
{
$raw = file_get_contents('php://input');
if (empty($raw))
{
return [];
}
return json_decode($raw, true);
}
private function getBodyParam($name, $defaultValue = null)
{
$data = $this->getBodyParams();
return isset($data[$name]) ? $data[$name] : $defaultValue;
}
/**
* 要求认证
* @return bool|array
* @throws MyHttpException
*/
private function requestAuth()
{
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW']))
{
try
{
$user = $this->user->login($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
return $user;
}
catch (MyHttpException $e)
{
if ($e->getStatusCode() != 422)
{
throw $e;
}
}
}
// 实现basic认证
header("WWW-Authenticate:Basic realm='Private'");
header('HTTP/1.0 401 Unauthorized');
print "You are unauthorized to enter this area.";
exit(0);
}
/**
* 处理访问用户的请求
*/
private function handleUser()
{
if ($this->method == 'GET')
{
if (empty($this->id))
{
//客户端需要获取所有用户的信息,访问有效,但是无权限
throw new MyHttpException(403);
}
static::sendHttpStatus(200, null, $this->user->view($this->id));
}
else if ($this->method == 'POST')
{
$data = $this->getBodyParams();
if (empty($data['username']))
{
throw new MyHttpException(422, '用户名不能为空');
}
if (empty($data['password']))
{
throw new MyHttpException(422, '密码不能为空');
}
static::sendHttpStatus(200, null, $this->user->register($data['username'], $data['password']));
}
else if ($this->method == 'PUT')
{
if (empty($this->id))
{
throw new MyHttpException(400);
}
$params = $this->getBodyParams();
if (empty($params) || empty($params['password']))
{
throw new MyHttpException(422, '密码不能为空');
}
//检测认证参数
$user = $this->requestAuth();
$result = $this->user->update($user['userId'], $params['password']);
static::sendHttpStatus(200, null, $result);
}
else
{
throw new MyHttpException(405);
}
}
private function getQueryParam($name, $defaultValue = null)
{
return isset($_GET[$name]) ? $_GET[$name] : $defaultValue;
}
/**
* 处理访问文章的请求
*/
private function handleArticle()
{
if ($this->method == 'GET')
{
$user = $this->requestAuth();
if (empty($this->id))
{
$list = $this->article->all($user['userId'], $this->getQueryParam('page', 1), $this->getQueryParam('size', 10));
self::sendHttpStatus(200, null, $list);
}
self::sendHttpStatus(200, null, $this->article->view($this->id));
}
else if ($this->method == 'POST')
{
$user = $this->requestAuth();
$data = $this->getBodyParams();
if (empty($data['title']))
{
throw new MyHttpException(422, '标题不能为空');
}
if (empty($data['content']))
{
throw new MyHttpException(422, '内容不能为空');
}
$article = $this->article->create($data['title'], $data['content'], $user['userId']);
static::sendHttpStatus(200, null, $article);
}
else if ($this->method == 'PUT')
{
if (empty($this->id))
{
throw new MyHttpException(400);
}
$user = $this->requestAuth();
$data = $this->getBodyParams();
$title = isset($data['title']) ? $data['title'] : null;
$content = isset($data['content']) ? $data['content'] : null;
$article = $this->article->update($this->id, $title, $content, $user['userId']);
static::sendHttpStatus(200, null, $article);
}
else if ($this->method == 'DELETE')
{
if (empty($this->id))
{
throw new MyHttpException(400);
}
$user = $this->requestAuth();
$this->article->delete($this->id, $user['userId']);
static::sendHttpStatus(204, null, null);
}
}
}
$bridge = new Bridge();
$bridge->handle();
1.php
<?php
class Restful
{
private $resouce;// 资源名称
private $id; // 资源id
private $requestMethod; // 请求的类型
private $setupRequestMethod;// 设置请求的类型
private $allowResouces = array("users", "aticles");
private $allowMethods = array("GET", "POST", "PUT", "DELETE", "OPTIONS");
// http状态码
private $statusCodes = array(
200 => "OK",
204 => "No Content",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
500 => "Server Internal Error",
);
// 初始化资源标识符
private function setupId(){}
/**
* 返回的json字符串
* @param $array
* @param int $code
*/
private function json($array, $code = 0)
{
// 对于a标签,如果链接的页面响应码为204,页面也不会发生跳转
if ($code > 0 && $code != 200 && $code != 204) {
// http/1.1 405 Method Not Allowed
header("HTTP/1.1 " . $code . " " . $this->statusCodes[$code]);
}
header("Content-Type:application/json;charset=utf-8");
// JSON_UNESCAPED_UNICODE 将json转为中文,而不是ASCII码
echo json_encode($array, JSON_UNESCAPED_UNICODE);
exit();
}
// 初始化请求方法
private function setupRequestMethod()
{
// 请求传递数据的方式
$this->setupRequestMethod = $_SESSION['REQUEST_METHOD'];
// $this->allowMethods array("GET", "POST", "PUT", "DELETE", "OPTIONS");
if (!in_array($this->setupRequestMethod, $this->allowMethods)) {
throw new Exception('请求方法不被允许', 405);
}
}
//初始化请求资源
private function setupResource()
{
$path = $_SESSION['PATH_INFO'];
$params = explode("/", $path);
// 请求资源url
$this->resouce = $params[1];// api.baidu.com//post/
if (!in_array($this->resouce, $this->allowResouces)) {
throw new Exception("请求资源不被允许", 405);
}
if (!empty($params[2])) {
// 请求资源的id
$this->id = $params[2];
}
}
/*****************************************************************************/
private function handleUser()
{
if ($this->requestMethod !="POST"){
throw new Exception("请求方法不被允许",405);
}
$body = $this->getBodyParams();
if (empty($body['username'])){
throw new Exception("不能为空",400);
}
if (empty($body['password'])){
throw new Exception("不能为空",400);
}
return $this->user->register($body['username'],$body['password']);
}
private function handleArticle()
{
switch ($this->requestMethod){
case "POST":
return $this->create();
break;
case "PUT":
return $this->edit();
break;
case "DELETE":
return $this->delete();
break;
case "GET":
if ($this->id){
return $this->list();
}else{
return $this->view();
}
break;
}
}
// 获取请求体
private function getBodyParams(){
/**
* 参考:https://blog.****.net/stand_forever/article/details/81736175
* php://input 不能用于 enctype="multipart/form-data"
* php://input读取不到$_GET数据
* php://input 通过输入流以文件读取方式取得未经处理的POST原始数据
*/
$rows = file_get_contents('php://input');
if (empty($rows)){
throw new Exception("请求参数错误",400);
}
return json_decode($rows,true);
}
public function run()
{
try {
$this->setupRequestMethod();
$this->setupResource();
$this->setupId();
if ($this->resouce == "users") {
return $this->handleUser();
} else {
return $this->handleArticle();
}
} catch (Exception $e) {
$this->json(array('error' => $e->getMessage()), $e->getCode());
}
}
}
$article = new Article($pdo);
$user = new User($pdo);
$restful = new Restful($user, $article);
$restful->run();