目录结构
首先创建如下目录结构:
my_framework
app
config
core
public
routes
storage
logs
tests
单一入口
所有请求通过单一入口进入,入口文件 public/index.php
自动加载
-
根目录执行
composer init
, 一路回车,即可生成一个composer.json
文件 -
composer install
, 生成vendor
目录,自动加载相关类库已生成 -
编辑
composer.json
,增加autoload.psr-4
目录映射"autoload": { "psr-4": { "App\\": "app/", "Core\\": "core/" } }, "autoload-dev": { "classmap": [ "tests/" ] }
-
执行
composer dump-autoload
重新加载映射类 -
index.php中引入自动加载
define('BASE_PATH', __DIR__); require __DIR__ . '/../vendor/autoload.php';
全局辅助函数及调试
-
composer require symfony/var-dumper
,后面可以像在Laravel中使用dd()/dump()
调试 -
创建
core/helpers.php
文件,在composer.json
中添加自动加载如下:"autoload": { "files": [ "core/helpers.php" ] },
-
然后执行
composer dump-autoload
服务容器 ☆
服务容器是此框架的核心,也是地基,所有功能模块都是建立在容器之上或之中的。服务容器类的核心思想是工厂模式。通过
bind()方法
, 将类的实例或类的映射等生成闭包注册到容器中,若绑定类的映射,服务容器会通过build()方法
利用反射自动完成对类的实例化。服务容器的好处包括但不限于:
- 预先绑定特定的类后,灵活性提高了,实现了解耦
- 后续可以根据类型或绑定时定义的别名,来实现自动的依赖注入
- 可以以单例的方式进行绑定,避免重复实例化,提升效率
-
composer require psr/container:1.1.1
-
创建
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; } ...
-
添加全局辅助函数
app()
,后续可用此方法使用服务容器if (!function_exists('app')) { function app($abstract = null) { if (!$abstract) return Application::getInstance(); return Application::getInstance()->get($abstract); } }
-
创建
core/Application.php
继承Container
class Application extends Container { public function __construct() { parent::__construct(); } }
Tips: 之所以再套一层,是因为后续在Application类中还会用来启动一些其他框架必须的服务,如注册基本的服务提供者等,这样子代码结构比较清晰。
-
在
index.php
中添加如下代码:// 实例化容器 $app = new \Core\Application();
-
在
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请求。包含但不限于:
启动Kennel中的启动引导类,如加载配置、异常处理、注册应用级ServiceProvider等
检验中间件
处理请求并返回结果
-
创建
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(); } }
-
index.php
追加$kernel = $app->get(\Core\Kernel\HttpKernel::class);
-
启动引导类实现如下:
所有的引导类都实现
BootstrapInterface
接口的bootstrap
方法
加载env
环境变量
-
composer require vlucas/phpdotenv
引入phpdotenv
包 -
根目录创建.env配置文件,配置格式同Laravel
-
创建
core\Bootstrap\LoadEnvironmentVariables.php
public function bootstrap() { $dotenv = Dotenv::createImmutable(__DIR__ . '/../../'); $dotenv->load(); }
-
添加全局辅助函数
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; } }
加载配置文件
-
创建
core/Config.php
核心配置类:public function get($abstract) { ... $fileArr = require_once __DIR__ . '/../config/' . $file . '.php'; ... }
-
创建
core\Bootstrap\LoadConfiguration.php
public function bootstrap() { app()->bind('config', Config::class, true); }
-
添加全局辅助函数
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
命令模式,会有不同的异常处理方式,所以使用接口形式绑定到容器
-
创建
app\Exceptions\HttpHandler.php
HTTP异常处理类,在app
目录创建可以方便用户自定义异常处理方式public function report(Throwable $e) { dump($e->getMessage()); } public function render(Throwable $e) { }
Tips: 此时只打印输出,后续接入Log、Response模块后再做其他处理
-
创建
core\Bootstrap\BindHttpExceptionHandler.php
,绑定异常处理类到接口进容器public function bootstrap() { // 绑定异常处理类 app()->bind(ExceptionHandler::class, HttpHandler::class, true); }
处理异常
-
创建
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()
方法,向容器中注册服务以及启动服务
-
创建
config/app.php
,格式如laravel
,直接返回:<?php return [ 'providers' => [ ] ];
-
创建
core\Bootstrap\RegisterProviders
:public function bootstrap() { $providers = config('app.providers'); foreach ($providers as $provider) { (new $provider())->register(); } }
-
后续新增服务提供者添加到
config/app.php
的providers
数组中即可
启动服务提供者
-
创建
core\Bootstrap\BootProviders
:public function bootstrap() { $providers = config('app.providers'); foreach ($providers as $provider) { (new $provider())->boot(); } }
日志
-
composer require monolog/monolog
-
创建
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 ], ], ];
-
创建日志类
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; } }
-
创建
LogServiceProvider
public function register() { app()->bind('logger', Logger::class, true); }
-
Application
启动类中注册LogServiceProvider
public function __construct() { parent::__construct(); $this->registerBaseServiceProviders(); } protected function registerBaseServiceProviders() { $this->registerAndBoot(new LogServiceProvider()); } protected function registerAndBoot($abstract) { $abstract->register(); $abstract->boot(); }
-
增加辅助函数
if (!function_exists('logger')) { //获取日志实例 function logger() { /** @var Core\Logger $logger */ $logger = app('logger'); return $logger->setLogger(); } }
-
修改
app\Exceptions\HttpHandler.php
的report()
方法,记录异常到日志public function report(Throwable $e) { logger()->error($exception->getMessage(), ['exception' => $e]); }
请求&响应
基于不重复造轮子的前提,我们引入相关
Symfony
包
-
composer require symfony/http-foundation
-
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; } }
-
创建
core/ResponseServiceProvider
public function register() { app()->bind('response', Response::class, true); }
-
Application
启动类中注册ResponseServiceProvider
protected function registerBaseServiceProviders() { $this->registerAndBoot(new LogServiceProvider()); $this->registerAndBoot(new ResponseServiceProvider()); }
-
增加辅助函数
if (!function_exists('response')) { //获取响应实例 function response() { /** @var Core\Http\Response $response */ $response = app('response'); return $response; } }
-
修改
app\Exceptions\HttpHandler.php
的render()
方法,增加异常响应渲染public function render(Throwable $e) { $response = response()->exception($e); $response->send(); }
-
HttpKernel
增加处理请求与响应的测试代码public function handle($request) { // 此部分代码会在完善路由、中间件等模块后修改,此时为了临时测试功能 $params = $request->query->all(); $response = response()->json(['code' => 1, 'msg' => 'success', 'data' => $params['name']]); return $response->prepare($request); }
-
index.php
增加获取HttpKernel
实例 && 调用请求处理方法 && 返回响应$kernel = $app->get(\Core\Kernel\HttpKernel::class); $response = $kernel->handle($request = \Core\Http\Request::capture()); $response->send();
-
测试请求与日志、响应
// 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
路由
-
composer require symfony/routing
-
创建
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); } }
-
创建
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() { } }
-
Application
启动类中注册RouterServiceProvider
protected function registerBaseServiceProviders() { $this->registerAndBoot(new LogServiceProvider()); $this->registerAndBoot(new ResponseServiceProvider()); $this->registerAndBoot(new RouterServiceProvider()); }
-
创建
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']); } } }
-
创建
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
目录下,方便用户更新或增加路由引入配置 -
修改
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); }
-
增加修改
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
函数大家可自行查阅资料
-
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); } }; }; } }
-
修改
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); }; }
-
修改
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); }
-
创建
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, ]; }
-
修改
public/index.php
实例化的Kernel
类$kernel = $app->get(\App\Http\Kernel::class);
-
创建测试中间件,路由定义中间件进行测试
// 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 [] []
命令行
-
composer require symfony/console
-
创建
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)); } } }
-
通过
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"; }
-
添加
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()'); } }
-
创建
core/Console/Command
的Trait
类(参考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; } }
-
创建
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
相当于
-
创建
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]; } }
-
创建
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()); ... } ...
-
创建
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() { } }
-
创建
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, ]
-
增加辅助函数,方便触发事件,进入监听
if (!function_exists('event')) { // 派发事件 function event($event, $payload = []) { /** @var Core\Event $events */ $events = app('events'); return $events->dispatch($event, $payload); } }
-
进行测试,创建
app/Events/ExampleEvent.php
和app/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