在不使用框架的情况下开始使用PHP 7+。
许多开发人员使用框架来开发和管理他们的PHP项目。然而,composer等软件包和新的PHP标准的引入使得开发者可以更容易地编写和维护他们的PHP代码。
这篇文章的主要目的是开发一个小型的Blog API,它具有独特的id 、title 、body 。这个Blog API的用户将列出、创建和查看一个新的博客文章。
我们将使用一个JSON文件作为数据库,因此所有的请求和响应都将是JSON格式。
前提条件
要跟上进度,你需要
- 安装最新版本的[PHP]。
- 安装了[PHP Composer]。
- 有关[PHP类]、[composer]和[MVC]模式的实际知识。
设置 composer
首先,我们将创建composer.json 文件。在添加第三方软件包和管理项目时,使用自动加载功能是必不可少的。
我们将创建我们的项目根目录,然后在终端运行下面的命令,并填写所需的信息或将它们保留为默认值。
$ composer init
上面的命令将在根目录下创建一个composer.json 文件。然后我们将创建另一个名为Application 的文件夹,并包括另外两个名为index.php 和app_config.php 的文件。
我们的项目文件夹结构将显示如下。
接下来,我们将通过在终端执行以下命令来添加我们的第一个包。
$ composer require monolog/monolog:1.25.1
上面的命令创建了一个vendor 文件夹,里面有monolog 包和一个名为autoload.php 的文件。
autoload.php 文件包含所有我们将从第三方添加的类的路径和我们的类,一个monolog 包协助生成日志文件。
接下来,我们将用以下代码编辑index.php 文件。
<?php
require __DIR__ . '/vendor/autoload.php';
然后,我们将修改我们的composer.json 文件,如下图所示。
"description": "Sample PHP Blog API",
"type": "project",
"autoload": {
"psr-4": {
"Application\\": "Application/"
}
},
我们将执行下面的命令来更新autoload 条目。
$ composer dump-autoload
autoload 条目注册了我们所有的类。与psr-0相比,psr - 4提供了更灵活的自动加载标准规范。
如果我们给我们的应用程序添加类,我们不需要重新生成自动加载器。
截至目前,我们的应用程序可以使用composer 。
为了验证这一点,我们可以在终端执行下面的命令。
$ php index.php
如果上面的命令没有返回错误,说明一切都在按预期工作。
添加第一个类
我们将处理整个项目所需的config 文件。注意,我们的项目中会有两个config 文件。
我们将从位于根目录下的app_config.php 文件开始。我们将把我们的应用程序的API Key,Cache setting, 和其他settings 放在这个文件中。
其他的config 文件将被放置在Application/Controller/App_config.php ,以读取变量。
我们将编辑位于项目根目录下的app_config.php 文件,如下所示。
<?php
return [
'LOG_PATH' => __DIR__ . './log-files',
];
接下来,我们将在目录Application/Controller/ 内创建一个新的App_config.php 文件,并在其中粘贴以下代码。
<?php namespace Application\Controller;
class App_config
{
private static $app_config;
public static function get($key, $default = null)
{
if (is_null(self::$app_config)) {
self::$app_config = require_once(__DIR__.'/../../app_config.php');
}
return !empty(self::$app_config[$key])?self::$app_config[$key]:$default;
}
}
上面的代码片段遍历了项目根目录下app_config.php 中的数组,并验证了数组中的一个键。
如果该键已经存在,它将返回一个值。否则,它返回指定的默认值。
接下来,我们将编辑index.php 文件,如下图所示。
<?php
require __DIR__ . '/vendor/autoload.php';
// New lines to be added
use Application\Controller\App_config;
$LOG_PATH = App_config::get('LOG_PATH', '');
echo "[LOG_PATH]: $LOG_PATH";
然后我们将执行下面的命令。
$ php index.php
上面的命令显示了在app_config.php 上指定的日志路径。
我们将在Application 文件夹中添加更多的类,由于有自动加载功能,它们将在应用程序的任何地方被访问。
添加日志
日志在任何应用模块中都是必不可少的,因为它可以确保一切都在按预期工作。
像monolog 这样的软件包可以简化日志记录。它们还可以将日志发送到电子邮件、slack、telegram和任何其他指定的平台。
在我们的应用程序中,我们将有三个日志文件,errors.log,request.log, 和app.log 。
errors.log 和request.log 文件将保持有效,而app.log 文件将用于显示所需的信息。
errors.log 文件将包含应用程序中可能发生的任何错误。request.log 文件记录了对应用程序的任何HTTP请求。
我们将在Application/Controller 文件夹中创建Logger.php 文件并粘贴下面的代码。
这将作为我们的包装器来管理我们不同的日志。
<?php namespace Application\Controller;
use Monolog\ErrorHandler;
use Monolog\Handler\StreamHandler;
class Logger extends \Monolog\Logger
{
private static $log_sys = [];
public function __construct($key = "app", $app_config = null)
{
parent::__construct($key);
if (empty($app_config)) {
$LOG_PATH = App_config::get('LOG_PATH', __DIR__ . '/../../log-files');
$app_config = [
'logFile' => "{$LOG_PATH}/{$key}.log",
'logLevel' => \Monolog\Logger::DEBUG
];
}
$this->pushHandler(new StreamHandler($app_config['logFile'], $app_config['logLevel']));
}
public static function getInstance($key = "app", $app_config = null)
{
if (empty(self:: $log_sys[$key])) {
self:: $log_sys[$key] = new Logger($key, $app_config);
}
return self:: $log_sys[$key];
}
public static function enableSystemLogs()
{
$LOG_PATH = App_config::get('LOG_PATH', __DIR__ . '/../../log-files');
// Error Log
self::$log_sys['error'] = new Logger('errors');
self:: $log_sys['error']->pushHandler(new StreamHandler("{$LOG_PATH}/errors.log"));
ErrorHandler::register(self::$log_sys['error']);
// Request Log
$data = [
$_SERVER,
$_REQUEST,
trim(file_get_contents("php://input"))
];
self::$log_sys['request'] = new Logger('request');
self::$log_sys['request']->pushHandler(new StreamHandler("{$LOG_PATH}/request.log"));
self::$log_sys['request']->info("REQUEST", $data);
}
}
在上面的代码中,我们已经创建了两个主要的功能;Logger::enableSystemLogs() ,它可以实现我们的错误和请求日志,然后Logger::getInstance() ,它将是我们的应用程序日志。
我们可以通过在我们的index.php 文件中添加以下几行代码来进行尝试。
<?php
require __DIR__ . '/vendor/autoload.php';
use Application\Controller\App_config;
$LOG_PATH = App_config::get('LOG_PATH', '');
echo "[LOG_PATH]: $LOG_PATH";
//New Lines
use Application\Controller\Logger;
Logger::enableSystemLogs();
$log_msg = Logger::getInstance();
$log_msg->info('Hello World');
然后我们将使用以下命令运行一个内置的PHP web服务器。
$ php -S localhost:8000
当你导航到<http://locahost:8000> ,你会看到LOG_PATH 。你也会注意到,我们在log-files 文件夹中有两个文件。
一个文件将显示requested content ,而另一个文件将包含Hello World 文本。
开发人员可以根据自己的需要改变请求,显示或删除特定的信息。
然后我们可以通过在Application/Controller 的位置创建一个名为App.php 的新文件来引导我们的应用程序,如下图所示。
<?php namespace Application\Controller;
class App
{
public static function run()
{
Logger::enableSystemLogs();
}
}
然后更新文件index.php ,如下所示。
<?php
require __DIR__ . '/vendor/autoload.php';
use Application\Controller\App;
App::run();
添加路由
路由在任何现代应用程序的开发中都是至关重要的。它意味着根据放置在URL中的路径来调用一个特定的代码块。
例如,/ 可以显示主页,而/post/1 则通过id 1 显示帖子信息。
在我们的案例中,我们将创建三个类Router.php,Request.php, 和Response.php 。
Router.php 验证请求方法,并使用regex匹配路径。如果匹配,它将运行一个回调函数,我们将使用两个参数指定;Request 和Response 。
Request.php 有一些方法可以获取请求中发送的数据。例如,POST数据,如标题和正文。
Response.php 将有一些函数来输出JSON 和特定的HTTP status 。
我们将在Application/Controller ,如下图所示,创建这三个文件。
Router.php
<?php namespace Application\Controller;
class Router
{
public static function get($app_route, $app_callback)
{
if (strcasecmp($_SERVER['REQUEST_METHOD'], 'GET') !== 0) {
return;
}
self::on($app_route, $app_callback);
}
public static function post($app_route, $app_callback)
{
if (strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') !== 0) {
return;
}
self::on($app_route, $app_callback);
}
public static function on($exprr, $call_back)
{
$paramtrs = $_SERVER['REQUEST_URI'];
$paramtrs = (stripos($paramtrs, "/") !== 0) ? "/" . $paramtrs : $paramtrs;
$exprr = str_replace('/', '\/', $exprr);
$matched = preg_match('/^' . ($exprr) . '$/', $paramtrs, $is_matched, PREG_OFFSET_CAPTURE);
if ($matched) {
// first value is normally the route, lets remove it
array_shift($is_matched);
// Get the matches as parameters
$paramtrs = array_map(function ($paramtr) {
return $paramtr[0];
}, $is_matched);
$call_back(new Request($paramtrs), new Response());
}
}
}
然后我们编写代码Request.php
<?php namespace Application\Controller;
class Request
{
public $paramtrs;
public $req_method;
public $content_type;
public function __construct($paramtrs = [])
{
$this->paramtrs = $paramtrs;
$this->req_method = trim($_SERVER['REQUEST_METHOD']);
$this->content_type = !empty($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
}
public function getBody()
{
if ($this->req_method !== 'POST') {
return '';
}
$post_body = [];
foreach ($_POST as $key => $value) {
$post_body[$key] = filter_input(INPUT_POST, $key, FILTER_SANITIZE_SPECIAL_CHARS);
}
return $post_body;
}
public function getJSON()
{
if ($this->req_method !== 'POST') {
return [];
}
if (strcasecmp($this->content_type, 'application/json') !== 0) {
return [];
}
// Receive the RAW post data.
$post_content = trim(file_get_contents("php://input"));
$p_decoded = json_decode($post_content);
return $p_decoded;
}
}
然后编写代码Response.php
<?php
namespace Application\Controller;
class Response
{
private $p_status = 200;
public function p_status(int $p_code)
{
$this->p_status = $p_code;
return $this;
}
public function toJSON($data = [])
{
http_response_code($this->p_status);
header('Content-Type: application/json');
echo json_encode($data);
}
}
我们将更新我们的index.php ,如下图所示。
<?php
require __DIR__ . '/vendor/autoload.php';
use Application\Controller\App;
use Application\Controller\Router;
use Application\Controller\Request;
use Application\Controller\Response;
Router::get('/', function () {
echo 'Hello World';
});
Router::get('/post/([0-9]*)', function (Request $request, Response $response) {
$response->toJSON([
'post' => ['id' => $request->paramtrs[0]],
'status' => 'ok'
]);
});
App::run();
然后我们将执行下面的命令来测试它。
$ php -S localhost:8000
我们将浏览到http://localhost:8000/,应该会显示一个Hello World 的信息。
如果我们浏览到http://localhost:8000/post/1,我们会得到以下JSON响应。
{"status": "ok", "post": { "id" : 1} }
上述结果表明,我们的应用程序有路由。
实现博客API
我们将实现三个端点。
GET /post- 显示所有的博客文章。POST /post- 协助创建一个新的帖子。GET /post/{id}- 显示特定的帖子。
我们将创建一个Posts 模型,并从我们的路由器调用它。我们将在Application/Model 目录中创建Posts.php 文件,并包括以下代码。
<?php namespace Application\Model;
use Application\Controller\App_config;
class Posts
{
private static $P_DATA = [];
public static function all()
{
return self::$P_DATA;
}
public static function add($b_post)
{
$b_post->id = count(self::$P_DATA) + 1;
self::$P_DATA[] = $b_post;
self::save();
return $b_post;
}
public static function findById(int $id)
{
foreach (self::$P_DATA as $b_post) {
if ($b_post->id === $id) {
return $b_post;
}
}
return [];
}
public static function load()
{
$DB_PATH = App_config::get('DB_PATH', __DIR__ . '/../../db.json');
self::$P_DATA = json_decode(file_get_contents($DB_PATH));
}
public static function save()
{
$DB_PATH = App_config::get('DB_PATH', __DIR__ . '/../../db.json');
file_put_contents($DB_PATH, json_encode(self::$P_DATA, JSON_PRETTY_PRINT));
}
}
然后,我们将在根目录下创建一个db.json 文件,内容如下。
[
{
"id": 2,
"title": "The Post 2",
"body": "The Post Content"
}
]
我们将编辑我们的app_config.php 文件,如下图所示。
<?php
return [
'LOG_PATH' => __DIR__ . './log-files',
'DB_PATH' => __DIR__ . '/db.json'
];
我们的数据库现在已经设置好了。我们可以用我们的路由器来使用它。我们可以修改index.php 文件来添加路由并调用数据库,如下图所示。
<?php
require __DIR__ . '/vendor/autoload.php';
use Application\Controller\App;
use Application\Controller\Router;
use Application\Controller\Request;
use Application\Controller\Response;
use Application\Model\Posts;
Posts::load();
Router::get('/post', function (Request $request, Response $response) {
$response->toJSON(Posts::all());
});
Router::post('/post', function (Request $request, Response $response) {
$b_post = Posts::add($request->getJSON());
$response->p_status(201)->toJSON($b_post);
});
Router::get('/post/([0-9]*)', function (Request $request, Response $response) {
$b_post = Posts::findById($request->paramtrs[0]);
if ($b_post) {
$response->toJSON($b_post);
} else {
$response->p_status(404)->toJSON(['error' => "Not Found"]);
}
});
App::run();
在上面的代码片断中,我们添加了Posts::load() 函数,从db.json 文件中加载我们的数据库。
我们使用了三个路由:GET /post ,列出现有的帖子,POST /post ,创建新的帖子,GET /post([0-9]\*) ,获取特定的帖子。
现在我们可以使用POSTMAN或curl来测试和模拟POST请求。在我们的例子中,我们将使用curl。我们将首先使用下面的命令启动我们的PHP服务器。
$ php -S localhost:8000
我们可以用下面的命令列出所有的帖子。
$ curl -X GET http://localhost:8000/post
输出结果是。
[{"id":2,"title":"The Post 2","body":"The Post Content"}]
然后我们可以用下面的命令列出一个帖子。
$ curl -X GET http://localhost:8000/post/2
而输出结果应该是:。
[{"id":2,"title":"The Post 2","body":"The Post Content"}]
我们还可以用下面的命令创建一个帖子。
$ curl -i -X POST -H "Content-Type: application/json" -d "{\"title\":\"Hello World\",\"body\":\"My Content\"}" http://localhost:8000/post
我们可以确认,该应用程序正在按预期工作。\
接下来,我们将进行一些测试。
测试
我们可以用用例和psr-2编码风格标准测试Router.php 文件。
我们将通过执行下面的命令向我们的项目添加更多的包。
$ composer require --dev squizlabs/php_codesniffer
$ composer require --dev peridot-php/peridot
$ composer require --dev peridot-php/leo
$ composer require --dev eloquent/phony-peridot
下面的命令检查我们代码语法的正确性。
$ composer ./vendor/bin/phpcs — standard=psr2 Application/
对于空白的错误,我们使用下面的命令来自动修复。
$ composer ./vendor/bin/phpcbf — standard=psr2 Application/
我们可以使用peridot进行单元测试,它提供了两个插件:leo--提供期望功能,以及phony-peridot,它提供的存根对于检查一个函数是否被调用至关重要。
我们将创建Test/Router.spec.php 文件并添加以下代码。
<?php namespace Application\Test;
use Application\Controller\Router;
use function Eloquent\Phony\stub;
describe("Application\\Controller\\Router", function () {
describe("->get", function () {
it("match regex and execute the callback", function () {
// Mock Request
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/post';
$stub = stub(function () { });
Router::get('/post', $stub);
$stub->called();
});
it("shouldn't execute the callback if not GET request method", function () {
// Mock Request
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REQUEST_URI'] = '/post';
$stub = stub(function () { });
Router::get('/post', $stub);
expect($stub->checkCalled())->to->be->null();
});
it("match regex and get params", function () {
// Mock Request
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/post/12';
$stub = stub(function ($req) { });
Router::get('/post/([0-9]*)', $stub);
$stub->called();
$request = $stub->firstCall()->argument();
expect($request->paramtrs[0])->to->be->equal("12");
});
});
describe("->post", function () {
it("match regex and execute the callback", function () {
// Mock Request
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REQUEST_URI'] = '/post';
$stub = stub(function () { });
Router::b_post('/post', $stub);
$stub->called();
});
it("shouldn't execute the callback if not POST request method", function () {
// Mock Request
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/post';
$stub = stub(function () { });
Router::b_post('/post', $stub);
expect($stub->checkCalled())->to->be->null();
});
});
});
我们还将编辑composer.json 文件,如下图所示。
"scripts": {
"test": [
"./vendor/bin/peridot Test/",
"./vendor/bin/phpcs --standard=psr2 Application/"
]
我们可以通过执行下面的命令来运行该测试。
$ composer test
将会显示以下输出。
如果有任何问题,将显示错误信息。否则,将不返回任何错误。
总结
我们已经成功实现了我们的Blog API项目。还有很多东西可以添加,但我们不能一下子实现它们,以保持一切简单。然而,学习者可以利用这些知识来制作更强大的应用程序。