Awesome
Application Context Manager for Laravel
We developed honeystone/context
for managing the application context in multi-tenant applications. It provides a
simple, fluent API for intializing, extending and switching contexts using 'context resolvers'. In addition, contexts
are automagically available in queued jobs and can be used to scope Eloquent models.
This package was developed several years ago for our own multi-tenant applications. We've recently decided to release it in the hope it will be useful to the wider Laravel community. We're open to contributions, feedback and constructive criticism.
Getting started
Start by reading the short blog post that demonstrates how to use this package in multi-tenant application.
Support us
We are committed to delivering high-quality open source packages maintained by the team at Honeystone. If you would like to support our efforts, simply use our packages, recommend them and contribute.
If you need any help with your project, or require any custom development, please get in touch.
Installation
composer require honeystone/context
Usage
A typical use-case would be to declare and initialize the context in your middleware. You can then access the context data like this:
$team = context('team');
$project = context('project');
Defining the context
Contexts need to be defined before they can be resolved:
context()->define()
->require('team', Team::class)
->require('project', Project::class);
These context definitions are 'required', so if they cannot be resolved a CouldNotResolveRequiredContextException
will
be thrown. For undefined contexts, a UndefinedContextException
will be thrown.
You can also define 'accepted', but not 'required' contexts:
context()->define()
->accept('team', Team::class)
->accept('project', Project::class);
Or accept / require based on the existence of another context:
context()->define()
->require('team', Team::class)
->requireWhenProvided('team', 'project', Project::class); //Or acceptWhenProvided
Initializing the context
To initialize the context, you'll need to provide a resolver:
context()->initialize(new MyResolver());
Here's an example resolver class:
<?php
declare(strict_types=1);
namespace App\Context\Resolvers;
use Honeystone\Context\ContextResolver;
class MyResolver extends ContextResolver
{
public function __construct(
private ?Team = null,
private ?Project = null,
) {}
public function resolveTeam(): Team
{
//your resolution logic
return $this->team;
}
public function resolveProject(): Project
{
//your resolution logic
return $this->project;
}
public static function deserialize(array $data): static
{
//you must also declare a deserialization method,
//to reinstate the serialised data
return new static($team, $project);
}
}
You can customize serialization logic like this:
class MyResolver extends ContextResolver
{
//...
public function serializeTeam(Team $team): int
{
return $team->id;
}
}
You can validate the integrity of a resolved context like this:
class MyResolver extends ContextResolver
{
//...
public function checkProject(
DefinesContext $definition,
Project $project,
array $resolved,
): bool {
return $project->id === $resolved['team']->project_id;
}
}
You can also reinitialize or deinitialize the current context:
context()->reinitialize(new AnotherResolver());
context()->deinitialize();
Extending the context
The current context can be extended using another resolver:
context()->define()->accept('site', Site::class);
context()->extend(new AnotherResolver());
Temporarily switching the context
Using a closure:
context()->initialize(new MyResolver());
context()->temporarilyInitialize(new AnotherResolver())
->run(function () {
//do something
});
Using the start and end methods:
context()->initialize(new MyResolver());
$tmpContext = context()->temporarilyInitialize(new AnotherResolver());
$tmpContext->start();
//do something
$tmpContext->end();
Events and receivers
The Honeystone\Context\Events\ContextChanged
event is dispatched whenever the context is changed.
You can also specify receivers to be notified when individual context values are set:
context()->receive('team', new MyReceiver());
Here's an example receiver:
<?php
declare(strict_types=1);
namespace App\Context\Receivers;
use Honeystone\Context\Contracts\ReceivesContext;
class TeamReceiver implements ReceivesContext
{
public function receiveContext(string $name, ?object $context): void
{
//process received context
}
}
Scoping models using the context
You can use the current context to scope your Eloquent models.
Here's an example:
<?php
declare(strict_types=1);
namespace App\Models;
use Honeystone\Context\Models\Concerns\BelongsToContext;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
use BelongsToContext;
protected static array $context = ['team'];
//optionally specify context aliases
protected static array $context = ['team' => 'my_team'];
//optionally configure a context foreign key
protected function getTeamContextForeignKey(Team $team): string
{
return 'my_team_id'; //the default is generated like this
}
//optionally configure a context owner id
protected function getTeamContextOwnerId(Team $team): int
{
return $context->id; //defaults to the id prop
}
//optionally configure the relationship name
protected function getTeamContextRelationName(Team $team): string
{
return 'my_team'; //the default is generated like this
}
}
Known issues
- Tests could do with improvement
Laravel's "context"
When this solution was created, Laravel context did not exist. The function name collision is unfortunate, but not really a problem. You'll just need to make sure you import the function.
use function Honeystone\Context\context;
Whilst the name will invite comparison, these packages are solving different problems. Laravel context is a generic global data store. This package is specifically for resolving context objects with more complex logic and using them to scope the application, for example in a multi-tenancy application.
Changelog
A list of changes can be found in the CHANGELOG.md file.