怎么从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();
    }
相关推荐
残月只会敲键盘40 分钟前
php代码审计--常见函数整理
开发语言·php
ac-er88881 小时前
MySQL如何实现PHP输入安全
mysql·安全·php
YUJIANYUE5 小时前
PHP将指定文件夹下多csv文件[即多表]导入到sqlite单文件
jvm·sqlite·php
龙哥·三年风水14 小时前
群控系统服务端开发模式-应用开发-个人资料
分布式·php·群控系统
Dingww101118 小时前
梧桐数据库中的网络地址类型使用介绍分享
数据库·oracle·php
Genius Kim21 小时前
SpringCloud Sentinel 服务治理详解
spring cloud·sentinel·php
原机小子1 天前
城镇保障性住房管理:SpringBoot系统解决方案
数据库·spring boot·php
kali-Myon1 天前
NewStarCTF2024-Week5-Web&Misc-WP
前端·python·学习·mysql·web安全·php·web
DK七七1 天前
当今陪玩系统小程序趋势,陪玩系统源码搭建后的适用于哪些平台
小程序·php·uniapp
tekin1 天前
vscode php Launch built-in server and debug, PHP内置服务xdebug调试,自定义启动参数配置使用示例
ide·vscode·php·launch.json·runtimeargs·php内置服务自定义参数