速查手册 - 领域内核
除了通过路由来调用控制器方法(action),Spiral 还允许从服务中甚至是从其它的控制器中调用控制器方法(HMVC: Hierarchical Model View Controller)。但是对控制器方法的方法的调用必须通过 Spiral\Core\CoreInterface
来进行。CoreInterface
以及领域内核使开发者能够更改调用流程,以及为控制器实现特定于域的功能。
领域内核依赖
spiral/hmvc
组件。官方 Web 应用模板中已经默认包含了这个组件。
调用控制器方法
Spiral 中的控制器都是普通的类,供调度程序调用。框架本身并不提供路由和控制器之间的直接耦合。这样的实现方式下,就可以在必要的时候在代码中直接调用控制器中定义的方法。
namespace App\Controller;
use Spiral\Core\CoreInterface;
class HomeController
{
public function index(CoreInterface $core)
{
return "Index: " . $core->callAction(HomeController::class, "other", ["name" => "Antony"]);
// 这行代码最终返回 "Index: Hello, Antony"
}
public function other(string $name)
{
return sprintf("Hello, %s", $name);
}
}
默认状态下,CoreInterface
接口由 Spiral\Core\Core
这个类实现,它只提供方法注入。
内核拦截器
为了实现自定义的调用逻辑,需要借助 Spiral\Core\CoreInterceptorInterface
接口 Spiral\Core\InterceptableCore
类。
首先创建一个实现 Spiral\Core\CoreInterceptorInterface
的自定义拦截器:
namespace App\Interceptor;
use Spiral\Core\CoreInterceptorInterface;
use Spiral\Core\CoreInterface;
class CustomInterceptor implements CoreInterceptorInterface
{
public function process(string $controller, string $action, array $parameters, CoreInterface $core)
{
return 'intercepted: ' . $core->callAction($controller, $action, $parameters);
}
}
接下来通过创建一个 InterceptableCore
类的实例来使用我们的自定义拦截器:
namespace App\Controller;
use App\Interceptor\CustomInterceptor;
use Spiral\Core\CoreInterface;
use Spiral\Core\InterceptableCore;
class HomeController
{
public function index(CoreInterface $core)
{
$customCore = new InterceptableCore($core);
$customCore->addInterceptor(new CustomInterceptor());
// 截获执行结果:Hello, Antony
return $customCore->callAction(HomeController::class, "other", ["name" => "Antony"]);
}
public function other(string $name)
{
return sprintf("Hello, %s", $name);
}
}
通过拦截器可以针对控制器、控制器方法和参数进行改动。根据需要也可使用多个拦截器。
全局领域内核
默认情况下,CoreInterface
只作用于 Spiral 框架路由的目标。通过把 Spiral\Core\CoreInterface
绑定到自定义的内核类,可以改变目标:
namespace App\Bootloader;
use App\Interceptor\CustomInterceptor;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\Core;
use Spiral\Core\CoreInterface;
use Spiral\Core\InterceptableCore;
class CoreBootloader extends Bootloader
{
protected const SINGLETONS = [
CoreInterface::class => [self::class, 'core']
];
private function core(Core $core): CoreInterface
{
$customCore = new InterceptableCore($core);
$customCore->addInterceptor(new CustomInterceptor());
return $customCore;
}
}
别忘了激活上面的引导程序,激活之后即可拦截所有的路由目标。
路由级领域内核
除了在引导程序中激活的全局领域内核以外,也可以只针对特定路由激活领域内核:
$customCore = new InterceptableCore($core);
$customCore->addInterceptor(new CustomInterceptor());
$router->setRoute(
'home',
new Route(
'/home/<action>',
(new Controller(HomeController::class))->withCore($customCore)
)
);
领域内核构建器
Spiral 提供了 Spiral\Bootloader\DomainBootloader
这个引导程序,可以很方便地自动配置核心拦截器,可以创建一个继承 DomainBootloader
的引导程序:
namespace App\Bootloader;
use App\Interceptor\CustomInterceptor;
use Spiral\Bootloader\DomainBootloader;
use Spiral\Core\CoreInterface;
class AppBootloader extends DomainBootloader
{
protected const SINGLETONS = [
CoreInterface::class => [self::class, 'domainCore']
];
protected const INTERCEPTORS = [
CustomInterceptor::class
];
}
把要配置的拦截器类添加到该引导程序的 INTEREPTORS
集合中,引导程序通过这一组默认拦截器即可全局性地配置应用程序行为。
Cycle 实体解析
使用 Spiral\Domain\CycleInterceptor
拦截器,可以根据参数自动解析和注入 Cycle 实体:
$router->setRoute(
'home',
new Route(
'/home/<action>/<id>',
new Controller(HomeController::class)
)
);
按照前文介绍的方法,把 CycleInterceptor
加入到默认拦截器集合中:
namespace App\Bootloader;
use Spiral\Bootloader\DomainBootloader;
use Spiral\Core\CoreInterface;
use Spiral\Domain\CycleInterceptor;
class AppBootloader extends DomainBootloader
{
protected const SINGLETONS = [
CoreInterface::class => [self::class, 'domainCore']
];
protected const INTERCEPTORS = [
CycleInterceptor::class
];
}
然后即可在控制器方法中直接依赖 Cycle 实体,系统会把路由中的 <id>
参数作为实体的主键,根据声明的类型自动解析出对应的实体对象并注入到方法。如果指定的主键没有查询到实体,会抛出 404 错误。
namespace App\Controller;
use App\Database\User;
class HomeController
{
public function index(User $user)
{
dump($user);
}
}
要使用的实体超过一个时,路由参数就不能再使用 <id>
,而必须使用命名参数:
$router->setRoute(
'home',
new Route(
'/home/<action>/<user>/<author>',
new Controller(HomeController::class)
)
);
控制器方法中的参数名要和路由参数名对应:
namespace App\Controller;
use App\Database\User;
use App\Database\Post;
class HomeController
{
public function index(User $user, User $author)
{
dump($user);
}
}
过滤器验证
Spiral 提供了 Spiral\Filter\FilterInterface
接口和 Spiral\Domain\FilterInterceptor
拦截器,开发者使用继承 Spiral\Filter\Filter
创建请求类,就可以实现请求参数的自动验证和过滤。如果参数检查失败,错误会以 JSON 形式返回(如果要改变这一行为,可以继承 FilterInterceptor
来创建自己的拦截器)。使用方法如下:
namespace App\Request;
use Spiral\Filters\Filter;
class LoginRequest extends Filter
{
public const SCHEMA = [
// 绑定参数与请求的关系
'username' => 'query:username',
'password' => 'query:password'
];
public const VALIDATES = [
// 定义验证规则
'username' => ['notEmpty'],
'password' => ['notEmpty']
];
}
在控制器方法中,依赖注入上面定义的 LoginRequest
, 系统会自动验证请求并过滤出需要的参数,然后通过 LoginRequest
对象传入:
namespace App\Controller;
use App\Request\LoginRequest;
class HomeController
{
public function index(LoginRequest $request)
{
dump($request);
}
}
Use
/home/index?username=n&password=p
即可通过上面的验证。
如果发生参数验证错误,客户端会收到类似下面实例的 application/json
类型的响应结果:
{
"status": 400,
"errors": {
"username": "This value is required.",
"password": "This value is required."
}
}
访问权限拦截器
使用 Spiral\Domain\GuardInterceptor
可以实现 RBAC(Role-Based Access Control: 基于角色的访问控制)预鉴权逻辑(需要安装和激活 spiral/security
组件)。
namespace App\Bootloader;
use Spiral\Bootloader\DomainBootloader;
use Spiral\Core\CoreInterface;
use Spiral\Domain\GuardInterceptor;
use Spiral\Security\Actor\Guest;
use Spiral\Security\PermissionsInterface;
use Spiral\Security\Rule;
class AppBootloader extends DomainBootloader
{
protected const SINGLETONS = [
CoreInterface::class => [self::class, 'domainCore']
];
protected const INTERCEPTORS = [
GuardInterceptor::class
];
public function boot(PermissionsInterface $rbac)
{
$rbac->addRole(Guest::ROLE);
$rbac->associate(Guest::ROLE, 'home.*', Rule\AllowRule::class);
$rbac->associate(Guest::ROLE, 'home.other', Rule\ForbidRule::class);
}
}
可以在控制器方法上用注释(annotation)声明访问权限:
namespace App\Controller;
use Spiral\Domain\Annotation\Guarded;
class HomeController
{
/**
* @Guarded("home.index")
*/
public function index()
{
return 'OK';
}
/**
* @Guarded("home.other")
*/
public function other()
{
return 'OK';
}
}
用注释声明访问权限时,也可以用 Guarded
注释的 else
属性来指定权限验证失败时应该调用的控制器方法:
/**
* @Guarded("home.other", else="notFound")
*/
public function other()
{
return 'OK';
}
注意:
else
属性可以使用的值包括:notFound
返回 404,forbidden
返回 401,error
返回 500,badAction
返回 400.
另外,使用 Spiral\Domain\Annotation\GuardNamespace
注释还可以给控制器指定 RBAC 命名空间,这样在各个方法的注释中就可以省略掉控制器前缀。在控制器指定了 RBAC 命名空间的情况下,方法上的 Guarded
注释可以省略参数,安全组件会自动使用 namespace.methodName
作为权限名称。
namespace App\Controller;
use Spiral\Domain\Annotation\Guarded;
use Spiral\Domain\Annotation\GuardNamespace;
/**
* @GuardNamespace("home")
*/
class HomeController
{
/**
* @Guarded()
*/
public function index()
{
return 'OK';
}
/**
* @Guarded(else="notFound")
*/
public function other()
{
return 'OK';
}
}
规则上下文
所有的路由参数(控制器方法参数)可以可以作为规则上下文调用。比如,创建一个规则类:
namespace App\Security;
use Spiral\Security\ActorInterface;
use Spiral\Security\RuleInterface;
class SampleRule implements RuleInterface
{
public function allows(ActorInterface $actor, string $permission, array $context): bool
{
return $context['user']->getID() !== 1;
}
}
在引导程序中激活这个规则类:
namespace App\Bootloader;
use App\Security\SampleRule;
use Spiral\Bootloader\DomainBootloader;
use Spiral\Core\CoreInterface;
use Spiral\Domain\CycleInterceptor;
use Spiral\Domain\GuardInterceptor;
use Spiral\Security\Actor\Guest;
use Spiral\Security\PermissionsInterface;
use Spiral\Security\Rule;
class AppBootloader extends DomainBootloader
{
protected const SINGLETONS = [
CoreInterface::class => [self::class, 'domainCore']
];
protected const INTERCEPTORS = [
CycleInterceptor::class,
GuardInterceptor::class
];
public function boot(PermissionsInterface $rbac)
{
$rbac->addRole(Guest::ROLE);
$rbac->associate(Guest::ROLE, 'home.*', SampleRule::class);
$rbac->associate(Guest::ROLE, 'home.other', Rule\ForbidRule::class);
}
}
注意:路由器中需要包括被调用的
<id>
或者<user>
参数。
然后控制器方法可以改为:
/**
* @Guarded()
*/
public function index(User $user)
{
return 'OK';
}
上述示例中,如果访问该方法(路由)的用户 id 不等于 1
,就会鉴权失败,被阻止访问。
由于这里的
User
依赖CycleInterceptor
进行解析,所以在领域核心中一定要在GuardInterceptor
之前启用CycleInterceptor
。
综合应用
把上面介绍到的所有拦截器综合到一起,就能实现丰富的领域逻辑和安全的控制器方法:
use Spiral\Bootloader\DomainBootloader;
use Spiral\Core\CoreInterface;
use Spiral\Domain;
class AppBootloader extends DomainBootloader
{
protected const SINGLETONS = [
CoreInterface::class => [self::class, 'domainCore']
];
protected const INTERCEPTORS = [
Domain\CycleInterceptor::class,
Domain\GuardInterceptor::class,
Domain\FilterInterceptor::class
];
}