Pending tenants
The process of creating tenants can be slow in some applications: a database may need to be created, migrated (potentially with hundreds of migrations), perhaps even seeded, or some external services may need to be called to provision some resources for the tenants (creating S3 buckets, issuing API keys, the list goes on).
To solve that problem, we have traditionally offered queued tenant creation. The problem with queued onboarding is that you then need to adjust your tenant onboarding flow since immediately upon signing up, the tenant’s database might not exist, so you need to handle a “preparing” state.
Pending tenants, introduced in version 4, are a new solution to this problem. Instead of using a queued onboarding process, you can simply maintain a pool of pre-created tenants. These tenants are created using a scheduled job and pruned during deployments. In the unlikely case the pool goes empty, the tenant creation process simply becomes synchronous — usually meaning just a bit slow.
To use pending tenants, add the HasPending trait to your Tenant model:
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;use Stancl\Tenancy\Database\Concerns\HasDatabase;use Stancl\Tenancy\Database\Concerns\HasDomains;use Stancl\Tenancy\Database\Concerns\HasPending;use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class Tenant extends BaseTenant implements TenantWithDatabase{ use HasDatabase, HasDomains, HasPending;}This trait will add some Eloquent scopes and helper methods to the model.
A tenant is marked as pending if its pending_since attribute is not null.
Note that thanks to the virtualcolumn feature, you do
not need a dedicated column for this attribute.
To create a pending tenant, you may use the createPending() method. This will
add a tenant to the “pool” of pending tenants. To pull a tenant from the pool,
you can use the pullPending($attributes = []) method. If the pool is empty, that
method will implicitly call create(), therefore the passed $attributes should
include values for all non-nullable columns. The
pullPendingFromPool($firstOrCreate = false, $attributes = []) method is called by
pullPending() and can be used instead of that higher-level method for more control
over whether a tenant should be created if the pool is empty or if null should be returned.
Or in code form:
// Empty pool
Tenant::createPending(['foo' => 'bar']);// Pool size = 1
// Pulled tenant will have both ['foo' => 'bar']// and ['company' => 'acme'] attributes$tenant = Tenant::pullPending(['company' => 'acme']);// Pool size = 0
// Pulled tenant will only have ['company' => 'acme']$tenant = Tenant::pullPending(['company' => 'acme']);// The pool is empty, so a tenant was created instead of pulling// Pool size = 0
$tenant = Tenant::pullPendingFromPool(false, ['company' => 'acme']);$tenant === null;The paragraph above mentioned that these calls should include all non-nullable columns
because if the pool is empty and the methods end up calling just Tenant::create(),
the passed attributes need to include everything necessary for the creation of a tenant
record in the database. (You should also include any information your own logic needs,
for instance if certain attributes are always expected to be present on the tenant in
a TenantCreated job pipeline.)
Similarly, the createPending() method should receive all attributes necessary for the
creation of the tenant (for instance if you have a non-nullable slug column, you can
set it to a dummy random string until that value is overwritten during a pull). This isn’t
always possible though, since as we’ll talk about in a moment, you will typically be creating
pending tenants using CLI commands that do not accept any attributes. Therefore, to provide
these default values for non-nullable columns, you can use the getPendingAttributes() method:
public function getPendingAttributes(array $attributes = []): array{ return ['slug' => Str::random(8)];}The $attributes argument is only informative. The array returned from this method
will be merged with $attributes, so the main use for $attributes is to return different
pending attributes dynamically based on the attributes being passed to createPending()
(which as mentioned above is generally not the case since you will be using a CLI command).
Console commands
Section titled “Console commands”To create pending tenants, typically in a scheduled job, you can use the tenants:pending-create
command:
tenants:pending-create--count= The number of pending tenants to maintainNotice the word maintain — the command doesn’t create new tenants if the pool is of the desired size already.
If the --count argument is not provided, the tenancy.pending.count config (or TENANCY_PENDING_COUNT
environment variable) is used.
To prune old pending tenants, typically during deployments, you can use the tenants:pending-clear
command:
tenants:pending-clear--older-than-days= Deletes all pending tenants older than the amount of days--older-than-hours= Deletes all pending tenants older than the amount of hoursQuery scopes
Section titled “Query scopes”The config key tenancy.pending.include_in_queries specifies whether pending tenants should be included
in tenant model queries by default. In other words, whether Tenant::all() should include pending tenants.
You may prefer disabling this config to avoid having to manually exclude (more on that below) tenants on
pages such as a central admin panel. Keep in mind that doing this will exclude pending tenants from commands
such as tenants:migrate. To work around that, you can either clear pending tenants (as suggested above)
during a deployment — which is when your migrations would be running — or you can use the --with-pending
option when calling tenants:migrate. Many commands that interact with multiple tenants support this
option, so make sure to run --help on those commands.
To manually include or exclude pending tenants, you can use the following query builder methods:
onlyPending()to only query pending tenantswithPending()to always include pending tenants, regardless of the config discussed abovewithoutPending()to always exclude pending tenants, again regardless of the config discussed above
Customizing the Eloquent cast
Section titled “Customizing the Eloquent cast”By default, the pending_since column is cast to timestamp. If you’d prefer using date, perhaps
with some specific format, you can set:
public static string $pendingSinceCast = 'date';