深入理解ThinkPHP6的APP类核心逻辑 | PHP框架教程

silverwq
2022-09-16 / 0 评论 / 300 阅读 / 正在检测是否收录...

APP类

一般来说,在index入口文件在加载autoLoad文件后,紧接着就是实例化一个App类,需要注意的是,这个App是可以扩展的,我们可以自定义一个类来继承它

class Erp extends App
{
    protected $bind = [
        'app'    => Erp::class,// 通过继承,替换掉系统的默认的类
        'cache'  => Cache::class,
        // 等等...,有很多默认设置好的绑定,这里列出几个举例子
    ]

    public function __construct(string $rootPath = '')
    {
        parent::__construct($rootPath);
        if (is_file($this->getBootstrapPath() . 'provider.php')) {
            $this->bind(include $this->getBootstrapPath() . 'provider.php');
        }
    }
}

然后创建一个boostrap目录,用于放一些启动文件,常见的有一下几个,不过可以自己增加其它的

  1. boostrap/app.php,用于实例化自定义类,然后直接在入口文件index.php里引入这个文件$app = require_once __DIR__.'/../bootstrap/app.php';
    
    $app = new \ship\foundation\Erp(
     realpath(__DIR__.'/../')
    );

return $app;

1. boostrap/provider.php,用于设置容器的绑定,修改request类为自定义的类,这个就是容器的强大扩展之处,可以不修改框架代码,实现自定义扩展
```php
return [
    'think\exception\Handle' => \ship\foundation\exceptions\HandleException::class,
    'think\Request' => \ship\foundation\AppRequest::class,
    'think\Validate' => \ship\foundation\validate\Validate::class,
    'console' => \ship\parents\console\AppConsole::class,
    'export' => \ship\foundation\Export::class,
    'event' => \ship\foundation\events\Event::class,
];
  1. boostrap/service.php,用于设置启动服务
    
    return [
     \ship\services\ShipService::class,
    ];

然后实例化app类的时候,会执行app类里的构造方法
```php
class App extends Container
{
   protected $bind = [
        'app'    => App::class,// 别名=>类明的形式,调用时候方便,$this->别名
        'cache'  => Cache::class,
        // 等等...,有很多默认设置好的绑定,这里列出几个举例子
    ]

 public function __construct(string $rootPath = '')
    {
        $this->thinkPath   = dirname(__DIR__) . DIRECTORY_SEPARATOR;
        $this->rootPath    = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
        $this->appPath     = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
        $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;

        if (is_file($this->appPath . 'provider.php')) {
         // 绑定自定义的类,或者覆盖系统默认的类,实现扩展
            $this->bind(include $this->appPath . 'provider.php');
        }

        static::setInstance($this);

        $this->instance('app', $this);
        $this->instance('think\Container', $this);
    }
}

由此构造方法可知,app类需要传入一个整个应用的根目录,然后所有其它的目录都是相对于这个根目录的,接着是是加载绑定文件,批量绑定。
这里的bind方法属于容器的方法,用于容器实例的代码定义的,可以绑定到类名、闭包、对象等,通过这个定义我们可以得到容器的实例,就好比docker容器里的容器shell脚本,通过这个脚本pull的时候,可以得到可以允许的容器实例。
接着是设置app容器实例,这个实例,可以最终被调用使用。

至此app的初始化应该完成了。

Container容器管理类

说到容器,我们首先想到的是docker里的容器,docker里有dockerFile文件,通过build这个dockerFile文件,可以得到镜像实例,然后这个镜像实例就可以运行了。这里的容器类也是实现类似的功能的,很多概念是互通的。

组件实例化

主要思路是,首先绑定一些组件别名到类、或者闭包、或者具体的对象实例;然后调用的时候,通过魔术方法make一个实例出来,如果实例已经存在,就直接返回实例。
make实例的时候:

  1. 如果发现组件是闭包,就直接调用,不过这个感觉没啥用,还不如直接定义一个函数调用下。
  2. 否则如果类名称,就去实例化这个类,不过好像传不了参数进去
class Container implements ContainerInterface, ArrayAccess, IteratorAggregate, Countable
{
    // 类似docker里的docker file的功能
    protected $bind = [];
    // 类型docker里的镜像的功能,组件的实例,组件可以是单例模式
    protected $instances = [];

    // 存下本身的实例,也就是容器本身也可以是单例模式
    protected static $instance;
    // app类在构造函数的时候static::setInstance($this);
    // 之后就可以这样使用了:Container::getInstance()->make($class, $args, $newInstance)
    public static function setInstance($instance): void
    {
        static::$instance = $instance;
    }
    // 单例模式
    public static function getInstance()
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }
        if (static::$instance instanceof Closure) {
            // 闭包一般代表可以自定义 
            return (static::$instance)();
        }
        return static::$instance;
    }

    // 返回别名对应的东西,比如类名、对象、或者闭包
    public function getAlias(string $abstract): string
    {
        if (isset($this->bind[$abstract])) {
            $bind = $this->bind[$abstract];

            if (is_string($bind)) {
                // 这里表示不同的别名可以对应同一个类,好像这也没啥用
                return $this->getAlias($bind);
            }
        }

        return $abstract;
    }

    // 绑定别名到类名、闭包、对象
    public function bind($abstract, $concrete = null)
    {
        if (is_array($abstract)) {
            foreach ($abstract as $key => $val) {
                $this->bind($key, $val);
            }
        } elseif ($concrete instanceof Closure) {
            $this->bind[$abstract] = $concrete;
        } elseif (is_object($concrete)) {
            $this->instance($abstract, $concrete);
        } else {
            $abstract = $this->getAlias($abstract);

            $this->bind[$abstract] = $concrete;
        }

        return $this;
    }

    //
    public function __get($name)
    {
        return $this->make($name);
    }

    // 类似docker里的build命令,用于将bind里定义的东西构建成实例
    public function make(string $abstract, array $vars = [], bool $newInstance = false)
    {
        $abstract = $this->getAlias($abstract);

        if (isset($this->instances[$abstract]) && !$newInstance) {
            // 单例模式,对于依赖注入的话,就方便的获取到已经实例化的组件
            return $this->instances[$abstract];
        }

        if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) {
            // 闭包直接调用
            $object = $this->invokeFunction($this->bind[$abstract], $vars);
        } else {
            // 对象或者类名
            $object = $this->invokeClass($abstract, $vars);
        }

        if (!$newInstance) {
            $this->instances[$abstract] = $object;
        }

        return $object;
    }
}

组件执行

主要是通过反射类,来执行组件:
如果绑定的是函数或者闭包,使用的是new ReflectionFunction($function);

// 调用闭包、或者全局函数,这个好像没啥用
// invokeFunction('fucntionName',[参数1]);
// invokeFunction(function(){},[参数1]);
public function invokeFunction($function, array $vars = [])
{
    try {
        $reflect = new ReflectionFunction($function);
    } catch (ReflectionException $e) {
        throw new FuncNotFoundException("function not exists: {$function}()", $function, $e);
    }

    // 返回的参数顺序函数的参数顺序
    $args = $this->bindParams($reflect, $vars);

    return $function(...$args);
}

如果绑定的是类名称,使用的是new ReflectionClass($class);;

// 类实例化
public function invokeClass(string $class, array $vars = [])
{
    try {
        $reflect = new ReflectionClass($class);
    } catch (ReflectionException $e) {
        throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
    }

    // 如果有__make方法,就不调用构造函数
    if ($reflect->hasMethod('__make')) {
        $method = $reflect->getMethod('__make');
        if ($method->isPublic() && $method->isStatic()) {
            $args = $this->bindParams($method, $vars);
            return $method->invokeArgs(null, $args);
        }
    }

    $constructor = $reflect->getConstructor();
    $args = $constructor ? $this->bindParams($constructor, $vars) : [];
    $object = $reflect->newInstanceArgs($args);
    $this->invokeAfter($class, $object);

    return $object;
}

参数绑定,这个很灵活,可以通过索引数组的方式设置参数,也可以通过关联数组的方式设置,这样的话就和顺序没有关系了,不过要注意参数是不是对象强类型,还可以依赖注入

protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = []): array
{
    if ($reflect->getNumberOfParameters() == 0) {
        return [];
    }

    // 判断数组类型 数字数组时按顺序绑定参数
    reset($vars);
    $type   = key($vars) === 0 ? 1 : 0;// 判断参数数组是否是关联数组0是,1不是
    $params = $reflect->getParameters();
    $args   = [];
    // 按方法参数顺序绑定
    foreach ($params as $param) {
        $name      = $param->getName();
        $lowerName = Str::snake($name);// 驼峰参数会自动转下划线
        $class     = $param->getClass();

        if ($class) {
            // 参数是类,如果此时$vars里的第一个是该类的实例则用这个,否则就实例化类
            $args[] = $this->getObjectParam($class->getName(), $vars);
        } elseif (1 == $type && !empty($vars)) {
            // 参数是索引数组,按顺序绑定
            $args[] = array_shift($vars);
        } elseif (0 == $type && isset($vars[$name])) {
            // 参数是关联数组,如果参数名称存在则绑定
            // 如果变量名称一样的话,会绑定到同一个参数
            $args[] = $vars[$name];
        } elseif (0 == $type && isset($vars[$lowerName])) {
            // 参数是关联数组,如果参数下划线的形式存在则绑定
            // 因为参数经常是驼峰,数组key经常是下划线
            $args[] = $vars[$lowerName];
        } elseif ($param->isDefaultValueAvailable()) {// 都没用的话用默认参数
            // 没有的话就取默认值
            $args[] = $param->getDefaultValue();
        } else {
            throw new InvalidArgumentException('method param miss:' . $name);
        }
    }
    return $args;
}

// 由这个可知,如果参数(类名称 $object)形式的话,顺序不能随便改变
// 因为判断第一个变量不是该类的对象的话就不会使用了
// 如果想要自动依赖注入的话,这个参数可以不传,也就是$vars的数量可以比参数的数量少
// 所以,如果不希望依赖注入,参数名称前面最好不要加入类名称,不然变量是关联数组参数的是,容易出现问题
protected function getObjectParam(string $className, array &$vars)
{
    $array = $vars;
    $value = array_shift($array);

    if ($value instanceof $className) {
        $result = $value;
        array_shift($vars);
    } else {
        // 依赖注入
        $result = $this->make($className);
    }

    return $result;
}

最后系统还提供了一个快捷的invoke方法,通过这个方法,可以参数绑定,调用的方法不存在时候,不会出现语法错误

public function invoke($callable, array $vars = [], bool $accessible = false)
{
    if ($callable instanceof Closure) {
        return $this->invokeFunction($callable, $vars);
    } elseif (is_string($callable) && false === strpos($callable, '::')) {
        return $this->invokeFunction($callable, $vars);
    } else {
        return $this->invokeMethod($callable, $vars, $accessible);
    }
}

// 这个方法好像跟组件没啥关系,组件实例化的时候用不到,只用invoke的时候才用到
// 不过这个方法,通过参数accessible,好像可以调用私有的方法,这点还不错
public function invokeMethod($method, array $vars = [], bool $accessible = false)
{
    if (is_array($method)) {
        [$class, $method] = $method;

        $class = is_object($class) ? $class : $this->invokeClass($class);
    } else {
        // 静态方法
        [$class, $method] = explode('::', $method);
    }

    try {
        $reflect = new ReflectionMethod($class, $method);
    } catch (ReflectionException $e) {
        $class = is_object($class) ? get_class($class) : $class;
        throw new FuncNotFoundException('method not exists: ' . $class . '::' . $method . '()', "{$class}::{$method}", $e);
    }

    $args = $this->bindParams($reflect, $vars);

    if ($accessible) {
        $reflect->setAccessible($accessible);
    }

    return $reflect->invokeArgs(is_object($class) ? $class : null, $args);
}

该方法经常用于参数绑定

// 调用对象的handle方法
$this->app->invoke([$obj, 'handle'], [$params]);
// 静态方法调用
$this->app->invoke("UserLogin::handle", [$params]);
// 闭包调用
$this->app->invoke(function($params){}, [$params]);
// 函数名称调用
$this->app->invoke('handle', [$params]);

服务

这个在lavarel里也叫做服务提供者,主要是提供一系列的服务,例如短信服务,支付服务等,提供一些和系统业务无关的一些基础服务。

protected $initializers = [
    Error::class,//异常类
    RegisterService::class,// 注册服务
    BootService::class,// 启动服务
];
// APP类->initialize方法里初始化系统的时候,会注册服务
foreach ($this->initializers as $initializer) {
    $this->make($initializer)->init($this);
}
public function register($service, bool $force = false)
{
    $registered = $this->getService($service);

    if ($registered && !$force) {
        return $registered;
    }

    // 注册服务,并且调用register方法,该方法主要是设置一些容器绑定
    if (is_string($service)) {
        $service = new $service($this);
    }
    if (method_exists($service, 'register')) {
        $service->register();
    }

    if (property_exists($service, 'bind')) {
        $this->bind($service->bind);
    }

    $this->services[] = $service;
}

然后在初始化,BootService::class启动服务

public function bootService($service)
{
    if (method_exists($service, 'boot')) {
        return $this->invoke([$service, 'boot']);
    }
}

boot方法里,主要是对服务组件的一些配置,为了提高性能,一般不实例化组件,等到用的时候再实例化,所以配置一般是配置到静态变量上

// 分页服务
class ValidateService extends Service
{
    public function register()
    {
        if (!$this->app->bound(Validate::class)) {
            $this->app->bind(Validate::class, Validate::class);
        }
    }

    public function boot()
    {
        Validate::maker(function (Validate $validate) {
            $validate->setLang($this->app->lang);
            $validate->setDb($this->app->db);
            $validate->setRequest($this->app->request);
        });
    }
}


class Validate {
    protected static $maker;

    public function __construct(){
        if (!empty(static::$maker)) {
            foreach (static::$maker as $maker) {
                call_user_func($maker, $this);
            }
        }
    }

    public static function maker(Closure $resolver)
    {
        static::$maker = $resolver;
    }
}
0

评论 (0)

取消