从零开始写PHP框架(参考Laravel设计)

项目完整代码地址:https://github.com/duwenfei/my_framework

目录结构

首先创建如下目录结构:

my_framework
    app
    config
    core
    public
    routes
    storage
        logs
    tests

单一入口

所有请求通过单一入口进入,入口文件 public/index.php

自动加载

  1. 根目录执行composer init, 一路回车,即可生成一个composer.json文件

  2. composer install, 生成vendor目录,自动加载相关类库已生成

  3. 编辑composer.json,增加autoload.psr-4目录映射

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Core\\": "core/"
        }
    },
    "autoload-dev": {
        "classmap": [
            "tests/"
        ]
    }
  4. 执行composer dump-autoload 重新加载映射类

  5. index.php中引入自动加载

    define('BASE_PATH', __DIR__);
    
    require __DIR__ . '/../vendor/autoload.php';

全局辅助函数及调试

  1. composer require symfony/var-dumper,后面可以像在Laravel中使用dd()/dump()调试

  2. 创建core/helpers.php文件,在composer.json中添加自动加载如下:

    "autoload": {
        "files": [
            "core/helpers.php"
        ]
    },
  3. 然后执行 composer dump-autoload


服务容器 ☆

服务容器是此框架的核心,也是地基,所有功能模块都是建立在容器之上或之中的。服务容器类的核心思想是工厂模式。通过 bind()方法, 将类的实例或类的映射等生成闭包注册到容器中,若绑定类的映射,服务容器会通过build()方法利用反射自动完成对类的实例化。服务容器的好处包括但不限于:

  1. 预先绑定特定的类后,灵活性提高了,实现了解耦
  2. 后续可以根据类型或绑定时定义的别名,来实现自动的依赖注入
  3. 可以以单例的方式进行绑定,避免重复实例化,提升效率
  1. composer require psr/container:1.1.1

  2. 创建 core/Container 容器类,实现Psr\Container\ContainerInterface接口

    <?php
    ...
    // 绑定对象,向容器中添加闭包实例,以供后续get()方法获取
    public function bind($abstract, $concrete, $is_singleton = false)
    {
        ...
        // 如果是类映射,那就通过反射类创建类的实例生成闭包
        if (!$concrete instanceof Closure) {
            $concrete = function () use ($concrete) {
                return $this->build($concrete);
            };
        }
        $this->bindings[$abstract] = compact('concrete', 'is_singleton');
    }
    
    // 获取已绑定在 $bindings 变量中的类,并将其返回实例化,其中 $is_singleton 可指定为单例对象。
    // 若为 true, 会将该类添加到 $instances 变量中,后续再调用该类时,直接从 $instances 变量中获取,类还是之前那个类。
    public function get(string $abstract, array $real_args = [])
    {
        //检查实例是否存在,已存在则直接返回
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }
        ...
        if (isset($this->bindings[$abstract]) && is_callable($this->bindings[$abstract]['concrete'])) {
            $instance = $this->call($this->bindings[$abstract]['concrete'], $real_args);
        } else {
            $instance = $this->build($abstract, $real_args);
        }
        return $instance;
    }
    
     // 反射方法并返回
    public function call(callable $callback, $real_args = [])
    {
        return $callback($real_args);
    }
    
    // 通过反射类创建类的实例
    public function build(string $class_name, array $real_args = [])
    {
        // 如果$abstract在$real_args的键中,用$real_args键值替换$abstract,
        // 用以实现无需预先bind(),直接从容器中get()获取实例时使用传参方式,绑定接口到实现
        if (isset($real_args[$abstract])) {
            $abstract = $real_args[$abstract];
        }
        $reflection = new ReflectionClass($class_name);
        ...
        return $reflection->newInstanceArgs($dependencies);
    }
    
    // 获取构建类所必要的参数信息
    private function getDependencies($parameters, $real_args = [])
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            ...
        }
        return $dependencies;
    }
    ...
  3. 添加全局辅助函数 app(),后续可用此方法使用服务容器

    if (!function_exists('app')) {
        function app($abstract = null)
        {
            if (!$abstract) return Application::getInstance();
            return Application::getInstance()->get($abstract);
        }
    }
  4. 创建core/Application.php继承Container

    class Application extends Container
    {
        public function __construct()
        {
            parent::__construct();
        }
    }

    Tips: 之所以再套一层,是因为后续在Application类中还会用来启动一些其他框架必须的服务,如注册基本的服务提供者等,这样子代码结构比较清晰。

  5. index.php 中添加如下代码:

    // 实例化容器
    $app = new \Core\Application();
  6. tests\ApplicationTest目录下添加测试类,测试结果如下:

    
    $app->bind(DataDumperInterface::class, \Symfony\Component\VarDumper\Dumper\CliDumper::class);
    dd($app->get(\Symfony\Component\VarDumper\Dumper\ServerDumper::class, [
        'host' => '127.0.0.1',
        // 'Symfony\Component\VarDumper\Dumper\DataDumperInterface' =>  \Symfony\Component\VarDumper\Dumper\CliDumper::class
        // DataDumperInterface::class =>  \Symfony\Component\VarDumper\Dumper\CliDumper::class
        // Symfony\Component\VarDumper\Dumper\DataDumperInterface::class =>  \Symfony\Component\VarDumper\Dumper\CliDumper::class
        'wrappedDumper' =>  \Symfony\Component\VarDumper\Dumper\CliDumper::class
    ]));
    
    $app->bind('str', function () {
        return 'str';
    });
    dump($app->get('str'));
    // str
    
    $app->bind('ApplicationTest\Go_To_School', Bicycle::class);
    $student = $app->get(Student::class);
    $student->go_to_school();
    // dwf
    // Bicycle to school
    
    $app->bind('ApplicationTest\Go_To_School', Bicycle::class);
    $app->bind('student', Student::class);
    $student = $app->get('student');
    $student->go_to_school();
    // dwf
    // Bicycle to school
    
    $student = $app->get(Student::class, [
        ApplicationTest\Go_To_School::class => Bicycle::class,
        'testDefaultParams' => 'qhy'
    ]);
    $student->go_to_school();
    // qhy
    // Bicycle to school

HTTP请求内核

HttpKernel

因为考虑到后续框架可能会支持Console命令模式,所以把不同方式请求抽离出来,负责接管HTTP请求。包含但不限于:

  1. 启动Kennel中的启动引导类,如加载配置、异常处理、注册应用级ServiceProvider等

  2. 检验中间件

  3. 处理请求并返回结果

  1. 创建core/Kernel/HttpKernel

    protected $bootstraps = [
        \Core\Bootstrap\LoadEnvironmentVariables::class,
        \Core\Bootstrap\LoadConfiguration::class,
        \Core\Bootstrap\BindHttpExceptionHandler::class,
        \Core\Bootstrap\HandleExceptions::class,
        \Core\Bootstrap\RegisterProviders::class,
        \Core\Bootstrap\BootProviders::class,
    ];
    
    public function __construct(Application $app)
    {
        $this->app = $app;
    
        $this->bootstrap();
    }
    
    // 注册启动引导类
    public function bootstrap()
    {
        foreach ($this->bootstraps as $bootstrapper) {
            $this->app->get($bootstrapper)->bootstrap();
        }
    }
  2. index.php 追加

    $kernel = $app->get(\Core\Kernel\HttpKernel::class);
  3. 启动引导类实现如下:

所有的引导类都实现BootstrapInterface接口的bootstrap方法

加载env环境变量

  1. composer require vlucas/phpdotenv 引入phpdotenv

  2. 根目录创建.env配置文件,配置格式同Laravel

  3. 创建core\Bootstrap\LoadEnvironmentVariables.php

    public function bootstrap()
    {
        $dotenv = Dotenv::createImmutable(__DIR__ . '/../../');
        $dotenv->load();
    }
  4. 添加全局辅助函数 env(),后续可用此方法获取.env中的私密配置

    if (!function_exists('env')){
        //获取env配置文件信息
        function env($abstract = null,$default = null){
            if (!$abstract){
                return null;
            }
            if (isset($_ENV[$abstract])){
                return $_ENV[$abstract];
            }
            return $default;
        }
    }

加载配置文件

  1. 创建 core/Config.php核心配置类:

    public function get($abstract)
    {
        ...
    
        $fileArr = require_once __DIR__ . '/../config/' . $file . '.php';
    
        ...
    }
  2. 创建 core\Bootstrap\LoadConfiguration.php

    public function bootstrap()
    {
        app()->bind('config', Config::class, true);
    }
  3. 添加全局辅助函数 config(),后续可用此方法获取config/**.php中的配置

    if (!function_exists('config')) {
        function config($name = null)
        {
            if (!$name) {
                return null;
            }
    
            /** @var \Core\Config $config */
            $config = app('config');
            return $config->get($name);
        }
    }

绑定异常处理类实体

因为后面可能会支持 Console 命令模式,会有不同的异常处理方式,所以使用接口形式绑定到容器

  1. 创建 app\Exceptions\HttpHandler.php HTTP异常处理类,在app目录创建可以方便用户自定义异常处理方式

    public function report(Throwable $e)
    {
        dump($e->getMessage());
    }
    
    public function render(Throwable $e)
    {
    
    }

    Tips: 此时只打印输出,后续接入Log、Response模块后再做其他处理

  2. 创建 core\Bootstrap\BindHttpExceptionHandler.php,绑定异常处理类到接口进容器

    public function bootstrap()
    {
        // 绑定异常处理类
        app()->bind(ExceptionHandler::class, HttpHandler::class, true);
    }

处理异常

  1. 创建 core\Bootstrap\HandleExceptions.php

    public function bootstrap()
    {
        error_reporting(-1);
        set_error_handler([$this, 'handleError']);
        set_exception_handler([$this, 'handleException']);
        register_shutdown_function([$this, 'handleShutdown']);
    }
    
    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
    
    public function handleException($e)
    {
        iif (! $e instanceof Exception) {
            $e = new Exception($e->getMessage() . ' | File: ' . $e->getFile() . ' Line: ' . $e->getLine());
        }
    
        // 报告或记录异常
        app()->get(ExceptionHandler::class)->report($e);
    
        // 渲染异常给用户
        app()->get(ExceptionHandler::class)->render($e);
    }
    
    public function handleShutdown()
    {
        if (!is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
            $this->handleException(new ErrorException($error, 0));
        }
    }
    
    protected function isFatal($type)
    {
        return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
    }

注册服务提供者 ☆

这里引入服务提供者的概念,每个服务添加一个服务提供者,服务提供者全部实现统一接口core/Providers/ServiceProviderInterface.php

其中主要实现 register() boot() 方法,向容器中注册服务以及启动服务

  1. 创建 config/app.php ,格式如 laravel ,直接返回:

    <?php
    
    return [    
        'providers' => [
    
        ]
    ];
  2. 创建 core\Bootstrap\RegisterProviders

    public function bootstrap()
    {
        $providers = config('app.providers');
        foreach ($providers as $provider) {
            (new $provider())->register();
        }
    }
  3. 后续新增服务提供者添加到 config/app.phpproviders数组中即可

启动服务提供者

  1. 创建 core\Bootstrap\BootProviders

    public function bootstrap()
    {
        $providers = config('app.providers');
        foreach ($providers as $provider) {
            (new $provider())->boot();
        }
    }

日志

  1. composer require monolog/monolog

  2. 创建config/logging.php配置文件

    <?php
    
    return [
        'default' => env('LOG_CHANNEL', 'single'),
    
        'channels' => [
            'single' => [
                'driver' => 'single',
                'path' => APP_BASE_PATH . '/storage/logs/' . env('APP_NAME', 'frame') . '.log',
                'level' => 'debug',
                'permission' => 0666
            ],
    
            'daily' => [
                'driver' => 'daily',
                'path' => APP_BASE_PATH . '/storage/logs/' . env('APP_NAME', 'frame') . '-'  . date('Y-m-d') . '.log',
                'level' => 'debug',
                'days' => 30,
                'permission' => 0666
            ],
        ],
    ];
  3. 创建日志类core/Logger.php

    class Logger
    {
        private $config;
    
        private $default_level = 'debug';
    
        public function __construct()
        {
            $this->config = config('logging.channels')[config('logging.default')];
        }
    
        public function setLogger()
        {
            /** @var MonologLogger $logger */
            $logger = app(MonologLogger::class, [
                'name'     => $this->config['driver'],
                'timezone' => config('app.timezone')
            ]);
    
            $logger->pushHandler(new StreamHandler(
                $this->config['path'],
                $this->config['level'] ?? $this->default_level
            ));
    
            return $logger;
        }
    }
  4. 创建LogServiceProvider

    public function register()
    {
        app()->bind('logger', Logger::class, true);
    }
  5. Application启动类中注册LogServiceProvider

    public function __construct()
    {
        parent::__construct();
    
        $this->registerBaseServiceProviders();
    }
    
    protected function registerBaseServiceProviders()
    {
        $this->registerAndBoot(new LogServiceProvider());
    }
    
    protected function registerAndBoot($abstract)
    {
        $abstract->register();
        $abstract->boot();
    }
  6. 增加辅助函数

    if (!function_exists('logger')) {
        //获取日志实例
        function logger()
        {
            /** @var Core\Logger $logger */
            $logger = app('logger');
            return $logger->setLogger();
        }
    }
  7. 修改 app\Exceptions\HttpHandler.phpreport() 方法,记录异常到日志

    public function report(Throwable $e)
    {
        logger()->error($exception->getMessage(), ['exception' => $e]);
    }

请求&响应

基于不重复造轮子的前提,我们引入相关Symfony

  1. composer require symfony/http-foundation

  2. core/Http/目录下创建 Request、Response 应用类,分别继承自 Symfony HTTP Foudation 组件的 Request、Response 基类

    use Symfony\Component\HttpFoundation\Request as BaseRequest;
    
    class Request extends BaseRequest
    {
        public static function capture()
        {
            static::enableHttpMethodParameterOverride();
            return static::createFromGlobals();
        }
    }
    use Symfony\Component\HttpFoundation\Response as BaseResponse;
    
    class Response extends BaseResponse
    {
        public function json($data = [])
        {
            $response = new BaseResponse(json_encode($data));
            $response->headers->set('Content-type', 'application/json');
            return $response;
        }
    
        public function success($data = [])
        {
            $rt = [
                'code' => 1,
                'msg'  => 'success',
            ];
            $rt = !empty($data) ? array_merge($rt, ['data' => $data]) : $rt;
            $response = new BaseResponse(json_encode($rt));
            $response->headers->set('Content-type', 'application/json');
            return $response;
        }
    
        public function exception(\Throwable $e)
        {
            $rt = [
                'code' => $e->getCode(),
                'msg'  => $e->getmessage() . ' | File: ' . $e->getFile() . ' Line: ' . $e->getLine(),
            ];
            $response = new BaseResponse(json_encode($rt));
            $response->headers->set('Content-type', 'application/json');
            return $response;
        }
    }
  3. 创建core/ResponseServiceProvider

    public function register()
    {
        app()->bind('response', Response::class, true);
    }
  4. Application启动类中注册ResponseServiceProvider

    protected function registerBaseServiceProviders()
    {
        $this->registerAndBoot(new LogServiceProvider());
        $this->registerAndBoot(new ResponseServiceProvider());
    }
  5. 增加辅助函数

    if (!function_exists('response')) {
        //获取响应实例
        function response()
        {
            /** @var Core\Http\Response $response */
            $response = app('response');
            return $response;
        }
    }
  6. 修改app\Exceptions\HttpHandler.phprender()方法,增加异常响应渲染

    public function render(Throwable $e)
    {
        $response = response()->exception($e);
        $response->send();
    }
  7. HttpKernel增加处理请求与响应的测试代码

    public function handle($request)
    {
        // 此部分代码会在完善路由、中间件等模块后修改,此时为了临时测试功能
        $params = $request->query->all();
        $response = response()->json(['code' => 1, 'msg' => 'success', 'data' => $params['name']]);
    
        return $response->prepare($request);
    }
  8. index.php增加获取HttpKernel实例 && 调用请求处理方法 && 返回响应

    $kernel = $app->get(\Core\Kernel\HttpKernel::class);
    
    $response = $kernel->handle($request = \Core\Http\Request::capture());
    $response->send();
  9. 测试请求与日志、响应

    // http://my_framework.com/?name=dwf
    // success
    {
        "code": 1,
        "msg": "success",
        "data": "dwf"
    }
    // exception
    {
        "code": 0,
        "msg": "Internal error: Failed to retrieve the default value"
    }
    // framework-2021-06-28.log
    [2021-06-28T12:05:17.141389+08:00] daily.ERROR: Internal error: Failed to retrieve the default value {"exception":"[object] (ReflectionException(code: 0): Internal error: Failed to

路由

  1. composer require symfony/routing

  2. 创建core/Http/Router.php

    <?php
    
    namespace Core\Http;
    
    use Symfony\Component\Routing\Matcher\UrlMatcher;
    use Symfony\Component\Routing\RequestContext;
    use Symfony\Component\Routing\Route as BaseRoute;
    use Symfony\Component\Routing\RouteCollection;
    
    class Router
    {
        public $attributes = [];
    
        /**
         * @Desc 支持路由GET方法
         */
        public function get($path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', $schemes = [], $methods = [], ?string $condition = '')
        {
            $route = new BaseRoute($path, $defaults, $requirements, $options, $host, $schemes, 'GET');
            $this->addToRouteCollection($path, $route);
        }
    
        /**
         * @Desc 支持路由POST方法
         */
        public function post($path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', $schemes = [], $methods = [], ?string $condition = '')
        {
            $route = new BaseRoute($path, $defaults, $requirements, $options, $host, $schemes, 'POST');
            $this->addToRouteCollection($path, $route);
        }
    
        /**
         * @Desc 添加路由到路由集合
         * @param $path
         * @param $route
         */
        protected function addToRouteCollection($path, $route)
        {
            /**
             * @var RouteCollection $subCollection
             */
            $subCollection = app()->get(RouteCollection::class);
            $subCollection->add($path, $route);
    
            if (isset($this->attributes['prefix'])) {
                $subCollection->addPrefix($this->attributes['prefix']);
            }
            if (isset($this->attributes['middleware'])) {
                $subCollection->addDefaults(['middleware' => $this->attributes['middleware']]);
            }
            if (isset($this->attributes['namespace'])) {
                $subCollection->addDefaults(['namespace' => $this->attributes['namespace']]);
            }
    
            app('routerCollection')->addCollection($subCollection);
        }
    
        /**
         * @Desc 支持路由组
         * @param array $attributes
         * @param $callback
         */
        public function group(array $attributes, $callback)
        {
            // 执行此路由组前缓存旧属性
            $old = $this->attributes;
    
            $this->attributes['namespace'] = $this->formatNamespace($attributes, $old);
            $this->attributes['prefix'] = $this->formatPrefix($attributes, $old);
    
            $callback(app('router'));
    
            // 执行此路由组后恢复旧属性
            $this->attributes = $old;
        }
    
        protected function formatNamespace($new, $old)
        {
            if (isset($new['namespace'])) {
                return isset($old['namespace']) && strpos($new['namespace'], '\\') !== 0
                    ? trim($old['namespace'], '\\').'\\'.trim($new['namespace'], '\\')
                    : trim($new['namespace'], '\\');
            }
    
            return $old['namespace'] ?? null;
        }
    
        protected function formatPrefix($new, $old)
        {
            if (isset($new['prefix'])) {
                return trim($old['prefix'] ?? null, '/').'/'.trim($new['prefix'], '/');
            }
    
            return $old['prefix'] ?? null;
        }
    
        /**
         * @Desc 派发请求
         * @param $request
         * @return mixed
         */
        public function dispatch(Request $request)
        {
            $context = new RequestContext();
            $context->fromRequest($request);
    
            // 初始化 UrlMatcher 对象
            $matcher = new UrlMatcher(app('routerCollection'), $context);
    
            // 查找当前路由
            $routeInfo = $matcher->match($request->getPathInfo());
    
            $controller = $routeInfo['namespace'] . '\\' . ltrim($routeInfo['controller'], '\\');
            $method = trim($routeInfo['method']);
    
            return app($controller)->$method($request);
        }
    }
  3. 创建core/Providers/RouterServiceProvider

    use Core\Http\Router;
    use Symfony\Component\Routing\RouteCollection;
    
    class RouterServiceProvider implements ServiceProviderInterface
    {
        public function register()
        {
            app()->bind('router', Router::class, true);
            app()->bind('routerCollection', RouteCollection::class, true);
        }
    
        public function boot()
        {
    
        }
    }
  4. Application启动类中注册RouterServiceProvider

    protected function registerBaseServiceProviders()
    {
        $this->registerAndBoot(new LogServiceProvider());
        $this->registerAndBoot(new ResponseServiceProvider());
        $this->registerAndBoot(new RouterServiceProvider());
    }
  5. 创建core/Providers/RouteServiceProvider

    class RouteServiceProvider implements ServiceProviderInterface
    {
        public function boot()
        {
            /** @var \Core\Http\Router $router */
            $router = app('router');
    
            $this->loadRoutesMap($router);
    
            // 未定义'/'根路由时,默认指向WelcomeController->hello()方法
            // 如果不做处理&&未定义'/'根路由,Symfony\Component\Routing\Matcher->match() Line:93 会throw new NoConfigurationException(),因为错误信息为空,定位问题不方便)
            if (!in_array('/', array_keys(app('routerCollection')->all()))) {
                $router->get('/', ['namespace' => 'Core\Http\Controller', 'controller' => 'WelcomeController', 'method' => 'hello']);
            }
            if (!in_array('/', array_keys(app('routerCollection')->all()))) {
                $router->get('/', ['namespace' => 'Core\Http\Controller', 'controller' => 'WelcomeController', 'method' => 'hello']);
            }
        }
    }
  6. 创建app/Providers/RouteServiceProvider继承core/Providers/RouteServiceProvider(需添加到config/app.php prividers数组中注册启动), routes/api.php(空文件即可)

    class RouteServiceProvider extends BaseRouteServiceProvider
    {
        public function boot()
        {
            parent::boot();
        }
    
        /**
         * @Desc 加载路由,如需增加路由文件,增加下列类似方法即可
         * @param Router $router
         */
        public function loadRoutesMap(Router $router)
        {
            $this->mapApiRoutes($router);
    
            $this->mapWebRoutes($router);
        }
    
        protected function mapApiRoutes(Router $router)
        {
            $router->group([
                'namespace' => 'App\Http\Controllers\Api',
                'prefix' => 'api',
            ], function ($router) {
            require APP_BASE_PATH . '/routes/api.php';
            });
        }
    }

    Tips: 之所以继承套了一层,是为了把app/Providers/RouteServiceProvider暴露到app目录下,方便用户更新或增加路由引入配置

  7. 修改core/Kernel/HttpKernel

    public function handle($request)
    {
        $response = $this->dispatchToRouter($request);
    
        if (is_null($response)) $response = response();
        return $response->prepare($request);
    }
    
    public function dispatchToRouter($request)
    {
        /* @var \Core\Http\Router $router */
        $router = app('router');
        return $router->dispatch($request);
    }
  8. 增加修改routes/api.php, App\Http\Controllers\TestApiController

    // routes/api.php
    <?php
    
    /** @var \Core\Http\Router $router */
    
    $router->group(['namespace' => 'Test', 'prefix' => 'test'], function ($router) {
        $router->get('/api1', ['controller' => 'TestApiController', 'method' => 'getTest']);
        $router->get('/api2', ['controller' => 'TestApi2Controller', 'method' => 'getList']);
    });
    $router->get('/api3', ['controller' => 'TestApi3Controller', 'method' => 'postSave']);
    
    // App\Http\Controllers\TestApiController
    class TestApiController
    {
        public function getTest(Request $request)
        {
            $params = $request->query->all();
            return response()->success($params);
        }
    }
    
    // 测试通过路由请求并返回结果
    // http://my_framework.com/api/test/api1?name=dwf
    {
        "code": 1,
        "msg": "success",
        "data": {
            "name": "dwf"
        }
    }

中间件

参照Laravel Pipeline简单实现,通过Pipeline,可以轻松实现AOP编程

核心思想就在于通过array_reduce,把闭包和中间件类生成一个匿名函数,然后调用执行

上面的闭包第一次将会是

有关array_reduce函数大家可自行查阅资料

  1. core/Pipeline

    class Pipeline
    {
        /**
         * 在每个管道上调用的方法名
         * @var string
         */
        protected $method = 'handle';
    
        /**
         * 通过管道传递的对象
         * @var mixed
         */
        protected $passable;
    
        /**
         * 存放管道的数组
         * @var array
         */
        protected $pipes = [];
    
        /**
         * 设置通过管道传递的对象(设置请求参数)
         * @param $passable
         * @return $this
         */
        public function send($passable)
        {
            $this->passable = $passable;
            return $this;
        }
    
        /**
         * 设置管道数组
         * @param array $pipes
         * @return $this
         */
        public function through($pipes)
        {
            $this->pipes = $pipes;
            return $this;
        }
    
        /**
         * 使用最终目标回调运行管道
         * @param Closure $destination
         * @return mixed
         */
        public function then(Closure $destination)
        {
            $pipeline = array_reduce(
                array_reverse($this->pipes), $this->getSlice(), $destination
            );
    
            return $pipeline($this->passable);
        }
    
        /**
         * 获取代表应用程序部分功能的闭包
         * @return Closure
         */
        protected function getSlice()
        {
            return function ($stack, $pipe) {
                return function ($passable) use ($stack, $pipe) {
                    if (is_callable($pipe)) { // 如果管道是可调用的,那么直接调用它
                        return $pipe($passable, $stack);
                    } else { // 否则容器解析管道并调用handle方法调用它
                        return app($pipe)->{$this->method}($passable, $stack);
                    }
                };
            };
        }
    }
  2. 修改core/Kernel/HttpKernel,使用Pipeline调用中间件过滤路由请求

    public function handle($request)
    {
        $response = (new Pipeline())->send($request)
            ->through($this->middleware)
            ->then($this->dispatchToRouter());
    
        // 兼容控制器无response返回
        if (is_null($response)) $response = response();
        return $response->prepare($request);
    }
    
    public function dispatchToRouter()
    {
        return function ($request) {
            /* @var \Core\Http\Router $router */
            $router = app('router');
            return $router->dispatch($request, $this->routeMiddleware);
        };
    }
  3. 修改core/Http/Router.php

    public function dispatch(Request $request, $routeMiddleware)
    {
        $context = new RequestContext();
        $context->fromRequest($request);
    
        // 初始化 UrlMatcher 对象
        $matcher = new UrlMatcher(app('routerCollection'), $context);
    
        // 查找当前路由
        $routeInfo = $matcher->match($request->getPathInfo());
    
        // 获取路由中间件别名实际的全限定类名
        $routeMiddlewareClass = [];
        if (isset($routeInfo['middleware'])) {
            foreach ($routeInfo['middleware'] as $middlewareAlias) {
                $routeMiddlewareClass[] = $routeMiddleware[$middlewareAlias];
            }
        }
    
        return (new Pipeline())->send($request)
            ->through($routeMiddleware)
            ->then(function ($request) use ($routeInfo) {
                return $this->runAction($routeInfo, $request);
            });
    }
    
    public function runAction($routeInfo, $request)
    {
        $controller = $routeInfo['namespace'] . '\\' . ltrim($routeInfo['controller'], '\\');
        $method = trim($routeInfo['method']);
    
        return app($controller)->$method($request);
    }
  4. 创建app/Http/Kernel继承core/Kernel/HttpKernel,暴露出去方便用户定义中间件

    class Kernel extends HttpKernel
    {
        protected $middleware = [
            \Core\Http\Middleware\CoreTestMiddleware::class,
        ];
    
        protected $routeMiddleware = [
            'apimiddleware1' => \App\Http\Middleware\TestRouteMiddleware1::class,
            'apimiddleware2' => \App\Http\Middleware\TestRouteMiddleware2::class,
        ];
    }
  5. 修改public/index.php实例化的Kernel

    $kernel = $app->get(\App\Http\Kernel::class);
  6. 创建测试中间件,路由定义中间件进行测试

    // core/Http/Middleware/CoreTestMiddleware.php
    class CoreTestMiddleware implements MiddlewareInterface
    {
        public function handle(Request $request, Closure $next)
        {
            logger()->info("开始-core-1");
            $res = $next($request);
            logger()->info("开始-core-2");
            return $res;
        }
    }
    
    // app/Http/Middleware/TestRouteMiddleware1.php
    class TestRouteMiddleware1
    {
        public function handle(Request $request, Closure $next)
        {
            logger()->info("开始-route-1");
            return $next($request);
        }
    }
    
    // app/Http/Middleware/TestRouteMiddleware2.php
    class TestRouteMiddleware2
    {
        public function handle(Request $request, Closure $next)
        {
            logger()->info("开始-route-2");
            return $next($request);
        }
    }
    
    // routes/api.php
    $router->group(['namespace' => 'Test', 'prefix' => 'test', 'middleware' => 'apimiddleware1|apimiddleware2'], function ($router) {
        $router->get('/api1', ['controller' => 'TestApiController', 'method' => 'getTest']);
    });
    
    // 测试结果
    // 访问:http://my_framework.com/api/test/api1
    // 查看日志如下:
    // [2021-07-02T01:00:53.026942+08:00] daily.INFO: 开始-core-1 [] []
    // [2021-07-02T01:00:53.028953+08:00] daily.INFO: 开始-route-1 [] []
    // [2021-07-02T01:00:53.029447+08:00] daily.INFO: 开始-route-2 [] []
    // [2021-07-02T01:00:53.030242+08:00] daily.INFO: 开始-core-2 [] []
    

命令行

  1. composer require symfony/console

  2. 创建 core\Kernel\ConsoleKernel

    class ConsoleKernel extends \Symfony\Component\Console\Application
    {
        private $app;
    
        protected $commands = [];
    
        protected $bootstraps = [
            \Core\Bootstrap\LoadEnvironmentVariables::class,
            \Core\Bootstrap\LoadConfiguration::class,
            \Core\Bootstrap\BindConsoleExceptionHandler::class,
            \Core\Bootstrap\HandleExceptions::class,
            \Core\Bootstrap\RegisterProviders::class,
            \Core\Bootstrap\BootProviders::class,
        ];
    
        public function __construct(Application $app)
        {
            $this->app = $app;
    
            $this->bootstrap();
    
            parent::__construct(config('app.name'), '1.0.0');
        }
    
        /**
         * @Desc 注册启动类
         */
        public function bootstrap()
        {
            foreach ($this->bootstraps as $bootstrapper) {
                $this->app->get($bootstrapper)->bootstrap();
            }
        }
    
        public function handle(InputInterface $input = null, OutputInterface $output = null)
        {
            $this->loadCommands();
    
            $exitCode = parent::run($input, $output);
    
            return $exitCode;
        }
    
        /**
         * @Desc 添加命令类实例到SymfonyConsoleApplication中
         */
        protected function loadCommands()
        {
            foreach ($this->commands as $command) {
                parent::add($this->app->get($command));
            }
        }
    }
  3. 通过 BindConsoleExceptionHandler 绑定 ConsoleHandler 到异常处理接口 ExceptionHandler 进容器,实现与HTTP请求不同的异常处理方式

    // core/Bootstrap/BindConsoleExceptionHandler.php
    
    public function bootstrap()
    {
        // 绑定异常处理类
        app()->bind(ExceptionHandler::class, ConsoleHandler::class, true);
    }
    
    // app/Exceptions/ConsoleHandler.php
    
    /**
     * 报告或记录异常
     */
    public function report(Throwable $e)
    {
        logger()->error($e->getMessage(), ['exception' => $e]);
    }
    
    /**
     * 将异常渲染到 HTTP 响应中
    */
    public function render(Throwable $e)
    {
        echo "\033[38;5;1;4m" . "Error: " . $e->getMessage() . ' | File: ' . $e->getFile() . ' Line: ' . $e->getLine() . "\033[0m";
    }
  4. 添加 core/Console/Command.php

    class Command extends SymfonyCommand
    {
        use IO, Parameters;
    
        protected $signature;
    
        protected $name;
    
        protected $description;
    
        protected $input;
    
        protected $output;
    
        public function __construct()
        {
            if (!isset($this->signature)) throw new Exception('Command must hava the attribute of signature');
    
            // 分解signature为名称,参数变量(参考Laravel实现)
            [$name, $arguments, $options] = self::parse($this->signature);
    
            parent::__construct($this->name = $name);
    
            // 设置命令描述
            $this->setDescription((string) $this->description);
    
            // 设置命令参数
            $this->getDefinition()->addArguments($arguments);
            $this->getDefinition()->addOptions($options);
        }
    
        /**
         * @Desc ConsoleKernel 继承的 Symfony\Component\Console\Application,最终会调用Command命令类的run方法
         * 在此处重载方法,定义输入输出的类属性值,方便后面用户命令类的handle方法获取命令参数
         */
        public function run(InputInterface $input, OutputInterface $output)
        {
            $this->input = $input;
            $this->output = $output;
    
            parent::run($input, $output);
        }
    
        /**
         * @Desc 上面的run方法最后会调用命令类的execute方法,在此处重载方法,用户只需定义handle方法即可
         */
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            $method = 'handle';
            return (int) $this->$method();
        }
    
        protected function handle()
        {
            throw new Exception('Command must have the method: handle()');
        }
    }
  5. 创建core/Console/CommandTrait类(参考Laravel实现)

    trait IO
    {
        /**
         * @var \Symfony\Component\Console\Input\InputInterface
         */
        protected $input;
    
        public function hasArgument($name)
        {
            return $this->input->hasArgument($name);
        }
    
        public function argument($key = null)
        {
            if (is_null($key)) {
                return $this->input->getArguments();
            }
    
            return $this->input->getArgument($key);
        }
    
        public function arguments()
        {
            return $this->argument();
        }
    
        public function hasOption($name)
        {
            return $this->input->hasOption($name);
        }
    
        public function option($key = null)
        {
            if (is_null($key)) {
                return $this->input->getOptions();
            }
    
            return $this->input->getOption($key);
        }
    
        public function options()
        {
            return $this->option();
        }
    }
    
    trait Parameters
    {
        public static function parse($expression)
        {
            $name = static::name($expression);
    
            if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches)) {
                if (count($matches[1])) {
                    return array_merge([$name], static::parameters($matches[1]));
                }
            }
    
            return [$name, [], []];
        }
    
        protected static function name($expression)
        {
            if (! preg_match('/[^\s]+/', $expression, $matches)) {
                throw new \Exception('Unable to determine command name from signature.');
            }
    
            return $matches[0];
        }
    
        protected static function parameters(array $tokens)
        {
            $arguments = [];
    
            $options = [];
    
            foreach ($tokens as $token) {
                if (preg_match('/-{2,}(.*)/', $token, $matches)) {
                    $options[] = static::parseOption($matches[1]);
                } else {
                    $arguments[] = static::parseArgument($token);
                }
            }
    
            return [$arguments, $options];
        }
    
        protected static function parseArgument($token)
        {
            [$token, $description] = static::extractDescription($token);
    
            switch (true) {
                case self::endsWith($token, '?*'):
                    return new InputArgument(trim($token, '?*'), InputArgument::IS_ARRAY, $description);
                case self::endsWith($token, '*'):
                    return new InputArgument(trim($token, '*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description);
                case self::endsWith($token, '?'):
                    return new InputArgument(trim($token, '?'), InputArgument::OPTIONAL, $description);
                case preg_match('/(.+)\=\*(.+)/', $token, $matches):
                    return new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
                case preg_match('/(.+)\=(.+)/', $token, $matches):
                    return new InputArgument($matches[1], InputArgument::OPTIONAL, $description, $matches[2]);
                default:
                    return new InputArgument($token, InputArgument::REQUIRED, $description);
            }
        }
    
        /**
         * Parse an option expression.
         *
         * @param  string  $token
         * @return \Symfony\Component\Console\Input\InputOption
         */
        protected static function parseOption($token)
        {
            [$token, $description] = static::extractDescription($token);
    
            $matches = preg_split('/\s*\|\s*/', $token, 2);
    
            if (isset($matches[1])) {
                $shortcut = $matches[0];
                $token = $matches[1];
            } else {
                $shortcut = null;
            }
    
            switch (true) {
                case self::endsWith($token, '='):
                    return new InputOption(trim($token, '='), $shortcut, InputOption::VALUE_OPTIONAL, $description);
                case self::endsWith($token, '=*'):
                    return new InputOption(trim($token, '=*'), $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description);
                case preg_match('/(.+)\=\*(.+)/', $token, $matches):
                    return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
                case preg_match('/(.+)\=(.+)/', $token, $matches):
                    return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL, $description, $matches[2]);
                default:
                    return new InputOption($token, $shortcut, InputOption::VALUE_NONE, $description);
            }
        }
    
        protected static function extractDescription($token)
        {
            $parts = preg_split('/\s+:\s+/', trim($token), 2);
    
            return count($parts) === 2 ? $parts : [$token, ''];
        }
    
        public static function endsWith($haystack, $needles)
        {
            foreach ((array) $needles as $needle) {
                if (substr($haystack, -strlen($needle)) === (string) $needle) {
                    return true;
                }
            }
    
            return false;
        }
    }
  6. 创建app/Console/Commands/TestCommand.php

    use Core\Console\Command;
    
    class TestCommand extends Command
    {
        protected $signature = 'test:command
                            {height : 体重}
                            {--name= : 名称}
                            {--sex= : 性别}';
    
        protected $description = '测试命令';
    
        public function handle()
        {
            dd($this->arguments(), $this->options());
        }
    }
    
    测试结果如下:
    
    // $ php artisan test:command 80 --name=dwf --sex=男
    // array:2 [
    //   "command" => "test:command"
    //   "height" => "80"
    // ]
    // array:8 [
    //   "name" => "dwf"
    //   "sex" => "男"
    //   "help" => false
    //   "quiet" => false
    //   "verbose" => false
    //   "version" => false
    //   "ansi" => false
    //   "no-interaction" => false
    // ]

事件

Laravel 的事件其实就是观察者模式的一个典型应用,Event 相当于

  1. 创建 core/Event.php

    namespace Core;
    
    class Event
    {
        protected $listeners = [];
    
        /**
         * @Desc 注册所有事件和监听器到event实例$listeners属性中
         */
        public function listen($events, $listener)
        {
            foreach ((array) $events as $event) {
                $this->listeners[$event][] = $this->makeListener($listener);
            }
        }
    
        /**
         * @Desc 返回监听器闭包
         */
        protected function makeListener($listener)
        {
            return function ($event, $payload) use ($listener) {
    
                $callable = $this->createClassCallable($listener);
    
                return $callable(...array_values($payload));
            };
        }
    
        /**
         * @Desc 返回监听器实例和handle方法的数组callable
         */
        protected function createClassCallable($listener)
        {
            return [app()->get($listener), 'handle'];
        }
    
        /**
         * @Desc 触发事件到监听器
         */
        public function dispatch($event, $payload = [])
        {
            [$event, $payload] = $this->parseEventAndPayload($event, $payload);
    
            $responses = [];
    
            $listeners = $this->listeners[$event] ?? [];
            foreach ($listeners as $listener) {
                $response = $listener($event, $payload);
    
                // 如果从listener返回 false,停止将事件传播。否则将继续遍历并触发每个listener。
                if ($response === false) break;
    
                $responses[] = $response;
            }
    
            return $responses;
        }
    
        protected function parseEventAndPayload($event, $payload)
        {
            if (is_object($event)) {
                [$payload, $event] = [[$event], get_class($event)];
            }
    
            return [$event, self::wrap($payload)];
        }
    
        public static function wrap($value)
        {
            if (is_null($value)) {
                return [];
            }
    
            return is_array($value) ? $value : [$value];
        }
    }
  2. 创建 core/Providers/EventsServiceProvider.php,并在 Application 启动时注册,将事件类绑定到容器

    // core/Providers/EventsServiceProvider.php
    namespace Core\Providers;
    
    use Core\Event;
    
    class EventsServiceProvider implements ServiceProviderInterface
    {
    
        public function register()
        {
            app()->bind('events', Event::class, true);
        }
    
        public function boot()
        {
    
        }
    }
    
    // core/Application.php
    ...
    protected function registerBaseServiceProviders()
    {
        $this->registerAndBoot(new EventsServiceProvider());
        $this->registerAndBoot(new LogServiceProvider());
        ...
    }
    ...
  3. 创建 core/Providers/EventServiceProvider.php

    use Core\Event;
    
    class EventServiceProvider implements ServiceProviderInterface
    {
        protected $listeners = [];
    
        public function boot()
        {
            /** @var Event $eventClass */
            $eventClass = app('events');
    
            // 注册所有事件和监听器到event实例$listeners属性中
            foreach ($this->listeners as $event => $listeners) {
                foreach (array_unique($listeners) as $listener) {
                    $eventClass->listen($event, $listener);
                }
            }
        }
    
        public function register()
        {
    
        }
    }
  4. 创建 app/Providers/EventServiceProvider.php,暴露给用户,定义事件的触发与监听器关系, 并添加到 config/app.php providers 中,Kernel类启动时会注册并启动此服务,将所有事件和监听器到 event 实例 $listeners属性中。

    // app/Providers/EventServiceProvider.php
    use Core\Providers\EventServiceProvider as ServiceProvider;
    
    class EventServiceProvider extends ServiceProvider
    {
        protected $listeners = [
    
        ];
    
        public function boot()
        {
            parent::boot();
        }
    }
    
    // config/app.php
    
    'providers' => [
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
    ]
  5. 增加辅助函数,方便触发事件,进入监听

    if (!function_exists('event')) {
        // 派发事件
        function event($event, $payload = [])
        {
            /** @var Core\Event $events */
            $events = app('events');
            return $events->dispatch($event, $payload);
        }
    }
  6. 进行测试,创建 app/Events/ExampleEvent.phpapp/Listeners/ExampleListener.php,并在 app/Providers/EventServiceProvider.php 中注册。

    // app/Events/ExampleEvent.php
    class ExampleEvent
    {
        protected $exampleEventData;
    
        public function __construct($exampleEventData)
        {
            $this->exampleEventData = $exampleEventData;
        }
    
        public function getEventData()
        {
            return $this->exampleEventData;
        }
    }
    
    // app/Listeners/ExampleListener.php
    use App\Events\ExampleEvent;
    
    class ExampleListener
    {
        public function handle(ExampleEvent $event)
        {
            $data = $event->getEventData();
            dd($data);
        }
    }
    
    // app/Providers/EventServiceProvider.php
    protected $listeners = [
        ExampleEvent::class => [
            ExampleListener::class
        ],
    ];
    
    // app/Http/Controllers/Api/Test/TestApiController.php
    public function getTest(Request $request)
    {
        $params = $request->query->all();
        event(new ExampleEvent('Test: Event => Listener'));
    
        return response()->success($params);
    }
    
    // 访问:http://my_framework.com/api/test/api1
    // 输出内容如下:
    // Test: Event => Listener

You may also like...