怎么从0到1开发一个PHP框架-1?

写在前面

本人开发的框架在2021年年初开发完成,后面没有再做过任何维护和修改。是仅供大家参考交流的学习项目,请勿使用在生产环境,也勿用作商业用途。

框架地址: github.com/yijiebaiyi/...

整体思路

开发一款web框架,首先要考虑这个框架的整体运行架构,然后具体到那些功能的扩展。那么我开发框架的时候想的是,精简为主,实用为主。主要功能需要包括入口文件、路由解析、异常处理、日志记录、ORM、缓存、类依赖注入。

入口文件

入口文件需要定义全局变量,主要是核心框架文件的所在路径,然后,通过include_once引入框架核心类文件,初始化框架进行初始化操作。

PHP 复制代码
<?php

define("FAST_PATH", $_SERVER["DOCUMENT_ROOT"] . DIRECTORY_SEPARATOR . "fast");

// 初始化
include_once FAST_PATH . DIRECTORY_SEPARATOR . "App.php";
(new \fast\App())->init();

应用核心类

应用核心类主要是用来注册类的自动加载、加载环境变量文件、注册错误异常以及注册路由。下面是应用初始化init方法。

PHP 复制代码
    public function init()
    {
        if (false === $this->isInit) {
            define("DOCUMENT_ROOT", $_SERVER["DOCUMENT_ROOT"]);
            define("ROOT_PATH", $_SERVER["DOCUMENT_ROOT"]);
            define("RUNTIME_PATH", $_SERVER["DOCUMENT_ROOT"] . DIRECTORY_SEPARATOR . "runtime");
            define("APP_PATH", $_SERVER["DOCUMENT_ROOT"]);

            // 注册自动加载
            require_once FAST_PATH . DIRECTORY_SEPARATOR . "Autoload.php";
            (new Autoload())->init();

            // 注册配置
            (new Config())->init();

            // 加载env
            (new Env())->init();

            // 注册错误和异常
            (new Exception())->init();
            (new Error())->init();
            (new Shutdown())->init();

            // 检验运行环境
            $this->validateEnv();

            // 注册路由
            (new Route())->init();

            $this->isInit = true;
        }
    }

上面初始化的方法中,我们需要先判断框架是否已经初始化,如果已经初始化则不需要再进行操作了。init方法中所涉及到的类都在框架核心文件根目录下面,需要注意的是,一定要先注册自动加载,不然使用new 关键字生成对象就会报错。下面是自动加载类的自动加载方法。

PHP 复制代码
    public function init()
    {
        if (false === $this->isInit) {
            spl_autoload_register(array($this, 'autoload'));

            $this->isInit = true;
        }
    }

    /**
     * @var array 类加载次
     */
    private static array $loadedClassNum = [];

    /**
     * 自动加载
     * @param $name
     * @throws Exception
     */
    public static function autoload($name): void
    {
        if (trim($name) == '') {
            throw new Exception("No class for loading");
        }

        $file = self::formatClassName($name);

        if (isset(self::$loadedClassNum[$file])) {
            self::$loadedClassNum[$file]++;
            return;
        }
        if (!$file || !is_file($file)) {
            return;
        }
        // 导入文件
        include $file;

        if (empty(self::$loadedClassNum[$file])) {
            self::$loadedClassNum[$file] = 1;
        }
    }

    /**
     * 返回全路径
     * @param $className
     * @return string
     */
    private static function formatClassName($className): string
    {
        return $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $className . '.php';
    }

使用PHP提供的spl_autoload_register自动加载器函数,注册autoload方法实现自动加载,可以看到我们自动加载的类必须都在项目根目录下才可以实现。这是一个简单的约定。

加载配置

我们知道php使用include 导入文件是可以获取到文件的返回值的(如果有的话),所以使用php文件返回一个数组来实现项目的配置文件,框架里面支持默认的config.php文件,以及额外用户可以自定义的配置:extra.php。这个也是我们约定好的。

配置文件示例代码config.php:

PHP 复制代码
<?php

return [
    "Cache" => [
        "default" => "redis",
        "redis" => [
            "master" => [
                "pconnect" => false,
                "host" => "localhost",
                "port" => 6379,
                "timeout" => 0,
            ],
        ],
    ],
    "Log" => [
        "default" => "file",
        "file" => [
            "path" => RUNTIME_PATH
        ],
    ]
];

引入配置文件的关键代码:

PHP 复制代码
    /**
     * 加载配置
     * @param $filename
     */
    private static function addConfig($filename): void
    {
        $configArr = include_once($filename);
        if (is_array($configArr)) {
            self::$configs = Arr::arrayMergeRecursiveUnique(self::$configs, $configArr);
        }
    }

    /**
     * 导入配置
     * @param $paths
     */
    private static function importConfig($paths): void
    {
        foreach ($paths as $path) {
            self::addConfig($path);
        }
    }

加载环境变量

环境变量文件,我们默认的就是项目根目录的.env文件。.env文件配置项是标准的*.ini类型配置文件的书写方式,且.env文件里面的配置项不区分大小写 ,小写配置项最终会被转化成大写。.env文件的加载使用php的函数parse_ini_file来实现:

PHP 复制代码
    /**
     * 加载环境变量定义文件
     * @param string $file 环境变量定义文件
     * @return void
     */
    public static function load(string $file): void
    {
        $env = parse_ini_file($file, true) ?: [];
        static::set($env);
    }

框架支持环境变量的写入、读取和检测。

错误和异常

异常信息抓取到之后,我们将他格式化处理,主要记录异常码、异常文件和所在行号。然后将异常写入日志。(注意,如果是生产模式,需要关闭错误显示)

PHP 复制代码
    public static function handler($exception)
    {
        // 设置http状态码,发送header
        if (in_array($exception->getCode(), array_keys(Http::$httpStatus))) {
            self::$httpCode = $exception->getCode();
        } else {
            self::$httpCode = 500;
        }
        Http::sendHeader(self::$httpCode);

        // 异常信息格式化输出
        $echoExceptionString = "<b>message</b>:  {$exception->getMessage()}<br/>" .
            "<b>code</b>:  {$exception->getCode()}<br/>" .
            "<b>file</b>:  {$exception->getFile()}<br/>" .
            "<b>line</b>:  {$exception->getLine()}<br/>";

        $serverVarDump = Str::dump(false, $_SERVER);
        $postVarDump = Str::dump(false, $_POST);
        $filesVarDump = Str::dump(false, $_FILES);
        $cookieVarDump = Str::dump(false, $_COOKIE);

        $logExceptionString = "message:  {$exception->getMessage()}" . PHP_EOL .
            "code:  {$exception->getCode()}" . PHP_EOL .
            "file:  {$exception->getFile()}" . PHP_EOL .
            "line:  {$exception->getLine()}" . PHP_EOL .
            "\$_SERVER:  {$serverVarDump}" . PHP_EOL .
            "\$_POST:  {$postVarDump}" . PHP_EOL .
            "\$_COOKIE:  {$cookieVarDump}" . PHP_EOL .
            "\$_FILES:  {$filesVarDump}";
        Log::write($logExceptionString, Log::ERROR);

        // debug模式将错误输出
        if (static::isDebugging()) {
            if (self::$isJson) {
                echo Json::encode(["message" => $exception->getMessage(), "code" => 0]);
                App::_end();
            } else {
                echo $echoExceptionString;
            }
        }
    }

路由分发

路由的实现思路是:我们根据请求的地址,截取到请求的路径信息(根据PHP全局变量$_SERVER['PATH_INFO']获取),根据路径信息的格式,定位到某个控制器类的某个方法,然后将其触发。实现代码:

PHP 复制代码
    public function distribute()
    {
        // 解析path_info
        if (isset($_SERVER['PATH_INFO'])) {
            $url = explode('/', trim($_SERVER['PATH_INFO'], "/"));
            if (count($url) < 3) {
                $url = array_pad($url, 3, "index");
            }
        } else {
            $url = array_pad([], 3, "index");
        }

        // 获取类名和方法名
        $className = self::formatClassName($url);
        $actionName = self::formatActionName($url);

        if (!class_exists($className)) {
            throw new Exception("the controller is not exist: {$className}", 404);
        }

        $class = new $className();

        if (!is_callable([$class, $actionName])) {
            throw new Exception("the action is not exist: {$className} -> {$actionName}", 404);
        }

        if (!$class instanceof Controller) {
            throw new Exception("the controller not belongs to fast\\Controller: {$className}", 403);
        }

        // 将请求分发
        $class->$actionName();
    }
相关推荐
NiNg_1_23439 分钟前
ThinkPHP5基础入门
php
2401_857610032 小时前
SpringBoot实现:校园资料分享平台开发指南
服务器·spring boot·php
Q_w77422 小时前
一个真实可用的登录界面!
javascript·mysql·php·html5·网站登录
一丝晨光3 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
Eternal-Student7 小时前
everyday_question dq20240731
开发语言·arm开发·php
zhangphil9 小时前
Windows环境Apache httpd 2.4 web服务器加载PHP8:Hello,world!
php·apache·httpd
芒果作者17 小时前
dcatadmin 自定义登录页面
php
酷酷-21 小时前
彩虹易支付最新版源码及安装教程(修复BUG+新增加订单投诉功能)
html·php·bug
杨哥带你写代码1 天前
构建高效新闻推荐系统:Spring Boot的力量
服务器·spring boot·php
A 八方1 天前
【漏洞复现】泛微OA E-Office do_excel.php 任意文件写入漏洞
开发语言·安全·php