不要与 Laravel 的新上下文库混淆,该包可用于构建多上下文多租户应用程序。大多数多租户库本质上都有一个“租户”上下文,因此如果您需要多个上下文,事情可能会变得有点麻烦。这个新包解决了这个问题。
让我们看一个例子好吗?
示例项目
对于我们的示例应用程序,我们将拥有一个组织成团队的全球用户群,每个团队将有多个项目。这是许多软件即服务应用程序中相当常见的结构。
对于多租户应用程序来说,每个用户群都存在于一个租户上下文中并不罕见,但对于我们的示例应用程序,我们希望用户能够加入多个团队,所以它是全局用户群。
全球用户群与租户用户群图

作为 saas,团队很可能是计费实体(即席位),并且某些团队成员将被授予管理团队的权限。不过,我不会在此示例中深入探讨这些实现细节,但希望它提供一些额外的上下文。
安装
为了保持这篇文章的简洁,我不会解释如何启动你的 laravel 项目。已经有许多更好的资源可用,尤其是官方文档。我们假设您已经有一个 laravel 项目,其中包含用户、团队和项目模型,并且您已准备好开始实现我们的上下文包。
安装很简单 作曲家推荐:
这个库有一个方便的函数 context(),从 laravel 11 开始,它与 laravel 自己的 context 函数发生冲突。这其实并不是一个问题。您可以导入我们的函数:
| 1 | 
usefunctionhonestone\context\context;
 | 
 
或者直接使用 laravel 的依赖注入容器。在这篇文章中,我将假设您已导入该函数并相应地使用它。
型号
让我们从配置我们的团队模型开始:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 
<?php declare(strict_types=1);
 
 namespaceapp\models;
 
 useilluminate\database\eloquent\model;
 useilluminate\database\eloquent\relations\belongstomany;
 useilluminate\database\eloquent\relations\hasmany;
 
 classteam extendsmodel
 {
     protected$fillable= ['name'];
 
     publicfunctionmembers(): belongstomany
     {
         return$this->belongstomany(user::class);
     }
 
     publicfunctionprojects(): hasmany
     {
         return$this->hasmany(project::class);
     }
 }
 | 
 
团队有名称、成员和项目。在我们的应用程序中,只有团队成员才能访问该团队或其项目。
好吧,让我们看看我们的项目:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 
<?php declare(strict_types=1);
 
 namespaceapp\models;
 
 useilluminate\database\eloquent\model;
 useilluminate\database\eloquent\relations\belongsto;
 
 classproject extendsmodel
 {
     protected$fillable= ['name'];
 
     publicfunctionteam(): belongsto
     {
         return$this->belongsto(team::class);
     }
 }
 | 
 
一个项目有一个名字并且属于一个团队。
确定上下文
当有人访问我们的应用程序时,我们需要确定他们在哪个团队和项目中工作。为了简单起见,我们用路由参数来处理这个问题。我们还假设只有经过身份验证的用户才能访问该应用程序。
既不是团队也不是项目上下文: app.mysaas.dev
仅团队上下文: app.mysaas.dev/my-team
团队和项目上下文: app.mysaas.dev/my-team/my-project
我们的路线将如下所示:
| 1 2 3 4 5 6 7 8 9 10 | 
route::middleware('auth')->group(function() {
 
     route::get('/', dashboardcontroller::class);
 
     route::middleware(appcontextmiddleware::class)->group(function() {
 
         route::get('/{team}', teamcontroller::class);
         route::get('/{team}/{project}', projectcontroller::class);
     });
 });
 | 
 
考虑到命名空间冲突的可能性,这是一种非常不灵活的方法,但它使示例保持简洁。在现实世界的应用程序中,您需要稍微不同地处理这个问题,也许是 anothersaas.dev/teams/my-team/projects/my-project 或 my-team.anothersas.dev/projects/my-project。
我们应该首先看看我们的appcontextmiddleware。该中间件初始化团队上下文以及项目上下文(如果设置):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 
<?php declare(strict_types=1);
 
 namespaceapp\HTTP\middleware;
 
 usefunctionhonestone\context\context;
 
 classteamcontextmiddleware
 {
     publicfunctionhandle(request $request, closure $next): mixed
     {
         
         $teamid= $request->route('team');
         $request->route()->forgetparameter('team');
 
         $projectid= null;
 
         
         if($request->route()->hasparamater('project')) {
 
             $projectid= $request->route('project');
             $request->route()->forgetparameter('project');
         }
 
         
         context()->initialize(newappresolver($teamid, $projectid));
     }
 }
 | 
 
首先,我们从路线中获取团队 id,然后忘记路线参数。一旦参数进入上下文,我们就不需要到达控制器。如果设置了项目 id,我们也会提取它。然后,我们使用 appresolver 传递团队 id 和项目 id(或 null)来初始化上下文:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 
<?php declare(strict_types=1);
 
 namespaceapp\context\resolvers;
 
 useapp\models\team;
 usehoneystone\context\contextresolver;
 usehoneystone\context\contracts\definescontext;
 
 usefunctionhonestone\context\context;
 
 classappresolver extendscontextresolver
 {
     publicfunction__construct(
         privatereadonly int $teamid,
         privatereadonly ?int $projectid= null,
     ) {}
 
     publicfunctiondefine(definescontext $definition): void 
     {
         $definition
             ->require('team', team::class)
             ->accept('project', project::class);
     }
 
     publicfunctionresolveteam(): ?team
     {
         returnteam::with('members')->find($this->teamid);
     }
 
     publicfunctionresolveproject(): ?project
     {
         return$this->projectid ?: project::with('team')->find($this->projectid);
     }
 
     publicfunctioncheckteam(definescontext $definition, team $team): bool
     {
         return$team->members->find(context()->auth()->getuser()) !== null;
     }
 
     publicfunctioncheckproject(definescontext $definition, ?project $project): bool
     {
         return$project=== null || $project->team->id === $this->teamid;
     }
 
     publicfunctiondeserialize(array$data): self
     {
         returnnewstatic($data['team'], $data['project']);
     }
 }
 | 
 
这里还有更多事情要做。
define() 方法负责定义正在解析的上下文。团队是必需的并且必须是 team 模型,并且项目被接受(即可选)并且必须是 project 模型(或 null)。
resolveteam() 将在初始化时在内部调用。它返回 team 或 null。如果出现空响应,contextinitializer 将抛出 couldnotresolverequiredcontextexception。
resolveproject() 也将在初始化时在内部调用。它返回项目或 null。在这种情况下,空响应不会导致异常,因为定义不需要该项目。
解析团队和项目后,contextinitializer 将调用可选的 checkteam() 和 checkproject() 方法。这些方法执行完整性检查。对于 checkteam(),我们确保经过身份验证的用户是团队的成员,对于 checkproject(),我们检查项目是否属于团队。
最后,每个解析器都需要一个 deserialization() 方法。此方法用于恢复序列化上下文。最值得注意的是,当在排队作业中使用上下文时,会发生这种情况。
现在我们的应用程序上下文已经设置好了,我们应该使用它。
访问上下文
像往常一样,我们会保持简单,尽管有点做作。查看团队时,我们希望看到项目列表。我们可以构建我们的 teamcontroller 来处理这样的需求:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 
<?php declare(strict_types=1);
 
 namespaceapp\http\controllers;
 
 useilluminate\view\view;
 
 usefunctioncompact;
 usefunctionhonestone\context\context;
 usefunctionview;
 
 classteamcontroller
 {
     publicfunction__invoke(request $request): view
     {
         $projects= context('team')->projects;
 
         returnview('team', compact('projects'));
     }
 }
 | 
 
足够简单。属于当前团队上下文的项目将传递到我们的视图。想象一下,我们现在需要查询项目以获得更专业的视图。我们可以这样做:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 
<?php declare(strict_types=1);
 
 namespaceapp\http\controllers;
 
 useilluminate\view\view;
 
 usefunctioncompact;
 usefunctionhonestone\context\context;
 usefunctionview;
 
 classprojectquerycontroller
 {
     publicfunction__invoke(request $request, string $query): view
     {
         $projects= project::Where('team_id', context('team')->id)
             ->where('name', 'like', "%$query%")
             ->get();
 
         returnview('queried-projects', compact('projects'));
     }
 }
 | 
 
现在变得有点麻烦,而且很容易意外地忘记按团队“确定查询范围”。我们可以使用项目模型上的 belongstocontext 特征来解决这个问题:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 
<?php declare(strict_types=1);
 
 namespaceapp\models;
 
 usehoneystone\context\models\concerns\belongstocontext;
 useilluminate\database\eloquent\model;
 useilluminate\database\eloquent\relations\belongsto;
 
 classproject extendsmodel
 {
     usebelongstocontext;
 
     protectedstaticarray$context= ['team'];
 
     protected$fillable= ['name'];
 
     publicfunctionteam(): belongsto
     {
         return$this->belongsto(team::class);
     }
 }
 | 
 
所有项目查询现在都将由团队上下文获取,并且当前的团队模型将自动注入到新的项目模型中。
让我们简化该控制器:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 
<?php declare(strict_types=1);
 
 namespaceApp\Http\Controllers;
 
 useIlluminate\View\View;
 
 usefunctioncompact;
 usefunctionview;
 
 classProjectQueryController
 {
     publicfunction__invoke(Request $request, string $query): View
     {
         $projects= Project::where('name', 'like', "%$query%")->get();
 
         returnview('queried-projects', compact('projects'));
     }
 }
 | 
 
这就是大家
从这里开始,您只需构建您的应用程序即可。上下文很容易获得,您的查询是有范围的,排队的作业将自动访问调度它们的相同上下文。
并非所有与上下文相关的问题都得到解决。您可能想要创建一些验证宏来为您的验证规则提供一些上下文,并且不要忘记手动查询不会自动应用上下文。
如果您计划在下一个项目中使用此软件包,我们很乐意听取您的意见。随时欢迎反馈和贡献。