在不使用框架的情况下开始使用PHP 7+的方法

111 阅读4分钟

在不使用框架的情况下开始使用PHP 7+。

许多开发人员使用框架来开发和管理他们的PHP项目。然而,composer等软件包和新的PHP标准的引入使得开发者可以更容易地编写和维护他们的PHP代码。

这篇文章的主要目的是开发一个小型的Blog API,它具有独特的idtitlebody 。这个Blog API的用户将列出、创建和查看一个新的博客文章。

我们将使用一个JSON文件作为数据库,因此所有的请求和响应都将是JSON格式。

前提条件

要跟上进度,你需要

  • 安装最新版本的[PHP]。
  • 安装了[PHP Composer]。
  • 有关[PHP类]、[composer]和[MVC]模式的实际知识。

设置 composer

首先,我们将创建composer.json 文件。在添加第三方软件包和管理项目时,使用自动加载功能是必不可少的。

我们将创建我们的项目根目录,然后在终端运行下面的命令,并填写所需的信息或将它们保留为默认值。

$ composer init

上面的命令将在根目录下创建一个composer.json 文件。然后我们将创建另一个名为Application 的文件夹,并包括另外两个名为index.phpapp_config.php 的文件。

我们的项目文件夹结构将显示如下。

folder structure

接下来,我们将通过在终端执行以下命令来添加我们的第一个包。

$ 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.logrequest.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匹配路径。如果匹配,它将运行一个回调函数,我们将使用两个参数指定;RequestResponse

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

将会显示以下输出。

app test

如果有任何问题,将显示错误信息。否则,将不返回任何错误。

总结

我们已经成功实现了我们的Blog API项目。还有很多东西可以添加,但我们不能一下子实现它们,以保持一切简单。然而,学习者可以利用这些知识来制作更强大的应用程序。