1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 09:34:04 +00:00

misc improvements - stronger types, exception refactor

This commit is contained in:
Samuel Štancl 2022-08-26 21:35:17 +02:00
parent ddc7cf49c3
commit 55d0a9ab87
34 changed files with 179 additions and 209 deletions

View file

@ -5,12 +5,6 @@ ARG PHP_VERSION=8.1
WORKDIR /var/www/html
LABEL org.opencontainers.image.source=https://github.com/stancl/tenancy \
org.opencontainers.image.vendor="Samuel Štancl" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="PHP ${PHP_VERSION} with modules for laravel support" \
org.opencontainers.image.description="PHP ${PHP_VERSION} with a set of php/os packages suitable for running Laravel apps"
# our default timezone and langauge
ENV TZ=Europe/London
ENV LANG=en_GB.UTF-8

View file

@ -6,11 +6,16 @@ namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\DatabaseConfig;
// todo possibly move to Database namespace, along with other classes
interface ManagesDatabaseUsers extends TenantDatabaseManager
{
/** Create a database user. */
public function createUser(DatabaseConfig $databaseConfig): bool;
/** Delete a database user. */
public function deleteUser(DatabaseConfig $databaseConfig): bool;
/** Does a database user exist? */
public function userExists(string $username): bool;
}

View file

@ -7,6 +7,8 @@ namespace Stancl\Tenancy\Contracts;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
// todo move all resource syncing-related things to a separate namespace?
/**
* @property-read Tenant[]|Collection $tenants
*/

View file

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts;
use Closure;
/**
* @see \Stancl\Tenancy\Database\Models\Tenant
*
* @method __call(string $method, array $parameters) IDE support. This will be a model.
* @method static __callStatic(string $method, array $parameters) IDE support. This will be a model.
* @mixin \Illuminate\Database\Eloquent\Model
*/
interface Tenant
@ -17,14 +17,14 @@ interface Tenant
public function getTenantKeyName(): string;
/** Get the value of the key used for identifying the tenant. */
public function getTenantKey();
public function getTenantKey(): int|string;
/** Get the value of an internal key. */
public function getInternal(string $key);
public function getInternal(string $key): mixed;
/** Set the value of an internal key. */
public function setInternal(string $key, $value);
public function setInternal(string $key, mixed $value): static;
/** Run a callback in this tenant's environment. */
public function run(callable $callback);
public function run(Closure $callback): mixed;
}

View file

@ -5,7 +5,50 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts;
use Exception;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
abstract class TenantCouldNotBeIdentifiedException extends Exception
abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution
{
/** Default solution title. */
protected string $solutionTitle = 'Tenant could not be identified';
/** Default solution description. */
protected string $solutionDescription = 'Are you sure this tenant exists?';
/** Set the message. */
protected function tenantCouldNotBeIdentified(string $how): static
{
$this->message = "Tenant could not be identified " . $how;
return $this;
}
/** Set the solution title. */
protected function title(string $solutionTitle): static
{
$this->solutionTitle = $solutionTitle;
return $this;
}
/** Set the solution description. */
protected function description(string $solutionDescription): static
{
$this->solutionDescription = $solutionDescription;
return $this;
}
/** Get the Ignition description. */
public function getSolution(): Solution
{
return BaseSolution::create($this->solutionTitle)
->setSolutionDescription($this->solutionDescription)
->setDocumentationLinks([
'Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants',
'Tenant Identification' => 'https://tenancyforlaravel.com/docs/v3/tenant-identification',
]);
}
}

View file

@ -8,24 +8,16 @@ use Stancl\Tenancy\Exceptions\NoConnectionSetException;
interface TenantDatabaseManager
{
/**
* Create a database.
*/
/** Create a database. */
public function createDatabase(TenantWithDatabase $tenant): bool;
/**
* Delete a database.
*/
/** Delete a database. */
public function deleteDatabase(TenantWithDatabase $tenant): bool;
/**
* Does a database exist.
*/
/** Does a database exist? */
public function databaseExists(string $name): bool;
/**
* Make a DB connection config array.
*/
/** Construct a DB connection config array. */
public function makeConnectionConfig(array $baseConfig, string $databaseName): array;
/**

View file

@ -11,5 +11,5 @@ interface TenantWithDatabase extends Tenant
public function database(): DatabaseConfig;
/** Get an internal key. */
public function getInternal(string $key);
public function getInternal(string $key): mixed;
}

View file

@ -6,26 +6,20 @@ namespace Stancl\Tenancy\Database\Concerns;
trait HasInternalKeys
{
/**
* Get the internal prefix.
*/
/** Get the internal prefix. */
public static function internalPrefix(): string
{
return 'tenancy_';
}
/**
* Get an internal key.
*/
public function getInternal(string $key)
/** Get an internal key. */
public function getInternal(string $key): mixed
{
return $this->getAttribute(static::internalPrefix() . $key);
}
/**
* Set internal key.
*/
public function setInternal(string $key, $value)
/** Set internal key. */
public function setInternal(string $key, mixed $value): static
{
$this->setAttribute(static::internalPrefix() . $key, $value);

View file

@ -4,15 +4,17 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Closure;
use Stancl\Tenancy\Contracts\Tenant;
trait TenantRun
{
/**
* Run a callback in this tenant's context.
* Atomic, safely reverts to previous context.
*
* This method is atomic and safely reverts to the previous context.
*/
public function run(callable $callback)
public function run(Closure $callback): mixed
{
/** @var Tenant $this */
$originalTenant = tenant();

View file

@ -14,6 +14,8 @@ use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException;
// todo move to Database namespace
/**
* @internal Class is subject to breaking changes in minor and patch versions.
*/

View file

@ -35,10 +35,8 @@ class ImpersonationToken extends Model
'created_at',
];
public static function boot()
public static function booted()
{
parent::boot();
static::creating(function ($model) {
$model->created_at = $model->created_at ?? $model->freshTimestamp();
$model->token = $model->token ?? Str::random(128);

View file

@ -39,7 +39,7 @@ class Tenant extends Model implements Contracts\Tenant
return 'id';
}
public function getTenantKey()
public function getTenantKey(): int|string
{
return $this->getAttribute($this->getTenantKeyName());
}

View file

@ -9,10 +9,8 @@ use Stancl\Tenancy\Contracts\Syncable;
class TenantPivot extends Pivot
{
public static function boot()
public static function booted()
{
parent::boot();
static::saved(function (self $pivot) {
$parent = $pivot->pivotParent;

View file

@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Scope;
class ParentModelScope implements Scope
{
public function apply(Builder $builder, Model $model)
public function apply(Builder $builder, Model $model): void
{
if (! tenancy()->initialized) {
return;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database;
use Closure;
use Illuminate\Database\Eloquent\Collection;
use Stancl\Tenancy\Contracts\Tenant;
@ -16,7 +17,7 @@ use Stancl\Tenancy\Contracts\Tenant;
*/
class TenantCollection extends Collection
{
public function runForEach(callable $callable): self
public function runForEach(Closure $callable): self
{
tenancy()->runForMultiple($this->items, $callable);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
@ -48,17 +49,17 @@ class DatabaseConfig
$this->tenant = $tenant;
}
public static function generateDatabaseNamesUsing(callable $databaseNameGenerator): void
public static function generateDatabaseNamesUsing(Closure $databaseNameGenerator): void
{
static::$databaseNameGenerator = $databaseNameGenerator;
}
public static function generateUsernamesUsing(callable $usernameGenerator): void
public static function generateUsernamesUsing(Closure $usernameGenerator): void
{
static::$usernameGenerator = $usernameGenerator;
}
public static function generatePasswordsUsing(callable $passwordGenerator): void
public static function generatePasswordsUsing(Closure $passwordGenerator): void
{
static::$passwordGenerator = $passwordGenerator;
}

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
// todo: in v4 this should be suffixed with Exception
class TenantCouldNotBeIdentifiedById extends TenantCouldNotBeIdentifiedException implements ProvidesSolution
{
public function __construct($tenant_id)
{
parent::__construct("Tenant could not be identified with tenant_id: $tenant_id");
}
public function getSolution(): Solution
{
return BaseSolution::create('Tenant could not be identified with that ID')
->setSolutionDescription('Are you sure the ID is correct and the tenant exists?')
->setDocumentationLinks([
'Initializing Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants',
]);
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
class TenantCouldNotBeIdentifiedByIdException extends TenantCouldNotBeIdentifiedException
{
public function __construct(int|string $tenant_id)
{
$this
->tenantCouldNotBeIdentified("by tenant id: $tenant_id")
->title('Tenant could not be identified with that ID')
->description('Are you sure the ID is correct and the tenant exists?');
}
}

View file

@ -4,24 +4,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution
class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifiedException
{
public function __construct($tenant_id)
public function __construct(int|string $tenant_id)
{
parent::__construct("Tenant could not be identified on path with tenant_id: $tenant_id");
}
public function getSolution(): Solution
{
return BaseSolution::create('Tenant could not be identified on this path')
->setSolutionDescription('Did you forget to create a tenant for this path?')
->setDocumentationLinks([
'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants/',
]);
$this
->tenantCouldNotBeIdentified("on path with tenant id: $tenant_id")
->title('Tenant could not be identified on this path')
->description('Did you forget to create a tenant for this path?');
}
}

View file

@ -4,24 +4,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution
class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeIdentifiedException
{
public function __construct($tenant_id)
public function __construct(mixed $payload)
{
parent::__construct("Tenant could not be identified by request data with payload: $tenant_id");
}
public function getSolution(): Solution
{
return BaseSolution::create('Tenant could not be identified with this request data')
->setSolutionDescription('Did you forget to create a tenant with this id?')
->setDocumentationLinks([
'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants/',
]);
$this
->tenantCouldNotBeIdentified("by request data with payload: $payload")
->title('Tenant could not be identified using this request data')
->description('Did you forget to create a tenant with this id?');
}
}

View file

@ -4,24 +4,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution
class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdentifiedException
{
public function __construct($domain)
public function __construct(string $domain)
{
parent::__construct("Tenant could not be identified on domain $domain");
}
public function getSolution(): Solution
{
return BaseSolution::create('Tenant could not be identified on this domain')
->setSolutionDescription('Did you forget to create a tenant for this domain?')
->setDocumentationLinks([
'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants/',
]);
$this
->tenantCouldNotBeIdentified("on domain $domain")
->title('Tenant could not be identified on this domain')
->description('Did you forget to create a tenant for this domain?');
}
}

View file

@ -8,18 +8,14 @@ use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
class TenantDatabaseAlreadyExistsException extends TenantCannotBeCreatedException
{
/** @var string */
protected $database;
public function __construct(
protected string $database,
) {
parent::__construct();
}
public function reason(): string
{
return "Database {$this->database} already exists.";
}
public function __construct(string $database)
{
$this->database = $database;
parent::__construct();
}
}

View file

@ -8,7 +8,7 @@ use Exception;
class TenantDatabaseDoesNotExistException extends Exception
{
public function __construct($database)
public function __construct(string $database)
{
parent::__construct("Database $database does not exist.");
}

View file

@ -8,18 +8,14 @@ use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
class TenantDatabaseUserAlreadyExistsException extends TenantCannotBeCreatedException
{
/** @var string */
protected $user;
public function __construct(
protected string $user,
) {
parent::__construct();
}
public function reason(): string
{
return "Database user {$this->user} already exists.";
}
public function __construct(string $user)
{
parent::__construct();
$this->user = $user;
}
}

View file

@ -15,7 +15,7 @@ class CrossDomainRedirect implements Feature
RedirectResponse::macro('domain', function (string $domain) {
/** @var RedirectResponse $this */
// replace first occurance of hostname fragment with $domain
// Replace first occurrence of the hostname fragment with $domain
$url = $this->getTargetUrl();
$hostname = parse_url($url, PHP_URL_HOST);
$position = strpos($url, $hostname);

View file

@ -13,9 +13,10 @@ use Stancl\Tenancy\Tenancy;
class UniversalRoutes implements Feature
{
public static $middlewareGroup = 'universal';
public static string $middlewareGroup = 'universal';
public static $identificationMiddlewares = [
// todo docblock
public static array $identificationMiddlewares = [
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
];
@ -39,7 +40,7 @@ class UniversalRoutes implements Feature
}
}
public static function routeHasMiddleware(Route $route, $middleware): bool
public static function routeHasMiddleware(Route $route, string $middleware): bool
{
if (in_array($middleware, $route->middleware(), true)) {
return true;

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Features;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Stancl\Tenancy\Contracts\Feature;
@ -14,7 +13,8 @@ use Stancl\Tenancy\Tenancy;
class UserImpersonation implements Feature
{
public static $ttl = 60; // seconds
/** The lifespan of impersonation tokens (in seconds). */
public static int $ttl = 60;
public function bootstrap(Tenancy $tenancy): void
{
@ -28,25 +28,20 @@ class UserImpersonation implements Feature
});
}
/**
* Impersonate a user and get an HTTP redirect response.
*
* @param string|ImpersonationToken $token
* @param int $ttl
*/
public static function makeResponse($token, int $ttl = null): RedirectResponse
/** Impersonate a user and get an HTTP redirect response. */
public static function makeResponse(string|ImpersonationToken $token, int $ttl = null): RedirectResponse
{
$token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token);
$ttl ??= static::$ttl;
if (((string) $token->tenant_id) !== ((string) tenant()->getTenantKey())) {
abort(403);
}
$tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl;
$ttl = $ttl ?? static::$ttl;
abort_if($tokenExpired, 403);
if ($token->created_at->diffInSeconds(Carbon::now()) > $ttl) {
abort(403);
}
$tokenTenantId = (string) $token->tenant_id;
$currentTenantId = (string) tenant()->getTenantKey();
abort_unless($tokenTenantId === $currentTenantId, 403);
Auth::guard($token->auth_guard)->loginUsingId($token->user_id);

View file

@ -11,14 +11,11 @@ use Stancl\Tenancy\Contracts\TenantResolver;
abstract class CachedTenantResolver implements TenantResolver
{
/** @var bool */
public static $shouldCache = false;
public static bool $shouldCache = false; // todo docblocks for these
/** @var int */
public static $cacheTTL = 3600; // seconds
public static int $cacheTTL = 3600; // seconds
/** @var string|null */
public static $cacheStore = null; // default
public static string|null $cacheStore = null; // default
/** @var Repository */
protected $cache;

View file

@ -11,21 +11,14 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
class DomainTenantResolver extends Contracts\CachedTenantResolver
{
/**
* The model representing the domain that the tenant was identified on.
*
* @var Domain
*/
public static $currentDomain;
/** The model representing the domain that the tenant was identified on. */
public static Domain $currentDomain; // todo |null?
/** @var bool */
public static $shouldCache = false;
public static bool $shouldCache = false;
/** @var int */
public static $cacheTTL = 3600; // seconds
public static int $cacheTTL = 3600; // seconds
/** @var string|null */
public static $cacheStore = null; // default
public static string|null $cacheStore = null; // default
public function resolveWithoutCache(...$args): Tenant
{

View file

@ -10,16 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
class PathTenantResolver extends Contracts\CachedTenantResolver
{
public static $tenantParameterName = 'tenant';
public static string $tenantParameterName = 'tenant';
/** @var bool */
public static $shouldCache = false;
public static bool $shouldCache = false;
/** @var int */
public static $cacheTTL = 3600; // seconds
public static int $cacheTTL = 3600; // seconds
/** @var string|null */
public static $cacheStore = null; // default
public static string|null $cacheStore = null; // default
public function resolveWithoutCache(...$args): Tenant
{

View file

@ -9,14 +9,11 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
class RequestDataTenantResolver extends Contracts\CachedTenantResolver
{
/** @var bool */
public static $shouldCache = false;
public static bool $shouldCache = false;
/** @var int */
public static $cacheTTL = 3600; // seconds
public static int $cacheTTL = 3600; // seconds
/** @var string|null */
public static $cacheStore = null; // default
public static string|null $cacheStore = null; // default
public function resolveWithoutCache(...$args): Tenant
{

View file

@ -11,7 +11,7 @@ use Illuminate\Support\Traits\Macroable;
use Stancl\Tenancy\Concerns\Debuggable;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedById;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
class Tenancy
{
@ -38,7 +38,7 @@ class Tenancy
$tenant = $this->find($tenantId);
if (! $tenant) {
throw new TenantCouldNotBeIdentifiedById($tenantId);
throw new TenantCouldNotBeIdentifiedByIdException($tenantId);
}
}
@ -62,17 +62,17 @@ class Tenancy
public function end(): void
{
event(new Events\EndingTenancy($this));
if (! $this->initialized) {
return;
}
event(new Events\TenancyEnded($this));
event(new Events\EndingTenancy($this));
$this->tenant = null;
$this->initialized = false;
$this->tenant = null;
event(new Events\TenancyEnded($this));
}
/** @return TenancyBootstrapper[] */
@ -131,7 +131,7 @@ class Tenancy
*
* @param Tenant[]|\Traversable|string[]|null $tenants
*/
public function runForMultiple($tenants, callable $callback): void
public function runForMultiple($tenants, Closure $callback): void
{
// Convert null to all tenants
$tenants = is_null($tenants) ? $this->model()->cursor() : $tenants;

View file

@ -2,6 +2,8 @@
declare(strict_types=1);
// todo likely move all of these classes to Database\
namespace Stancl\Tenancy\TenantDatabaseManagers;
use Illuminate\Database\Connection;
@ -12,10 +14,9 @@ use Stancl\Tenancy\Exceptions\NoConnectionSetException;
class MicrosoftSQLDatabaseManager implements TenantDatabaseManager
{
/** @var string */
protected $connection;
protected string $connection; // todo docblock, in all of these classes
protected function database(): Connection
protected function database(): Connection // todo consider abstracting this method & setConnection() into a base class
{
if ($this->connection === null) {
throw new NoConnectionSetException(static::class);
@ -33,7 +34,7 @@ class MicrosoftSQLDatabaseManager implements TenantDatabaseManager
{
$database = $tenant->database()->getName();
$charset = $this->database()->getConfig('charset');
$collation = $this->database()->getConfig('collation');
$collation = $this->database()->getConfig('collation'); // todo check why these are not used
return $this->database()->statement("CREATE DATABASE [{$database}]");
}

View file

@ -157,23 +157,25 @@ class AnotherTenant extends Model implements Contracts\Tenant
return 'id';
}
public function getTenantKey()
public function getTenantKey(): int|string
{
return $this->getAttribute('id');
}
public function run(callable $callback)
public function run(Closure $callback): mixed
{
$callback();
}
public function getInternal(string $key)
public function getInternal(string $key): mixed
{
return $this->$key;
}
public function setInternal(string $key, $value)
public function setInternal(string $key, mixed $value): static
{
$this->$key = $value;
return $this;
}
}