diff --git a/src/Database/Eloquent/Relations/BelongsToManyTenants.php b/src/Database/Eloquent/Relations/BelongsToManyTenants.php new file mode 100644 index 0000000..0087cd8 --- /dev/null +++ b/src/Database/Eloquent/Relations/BelongsToManyTenants.php @@ -0,0 +1,147 @@ + + */ +class BelongsToManyTenants extends BaseTenantRelationHandler +{ + /** + * Whether the relation populates before it's saved + * + * This method returns true if the relationship requires population + * before the model is persisted (creating event), or false if after + * (created event). + * + * @return bool + */ + public function populateBeforePersisting(): bool + { + return false; + } + + /** + * Populate the relationship to the tenant + * + * This method populates the tenant relationship with the current tenant, + * automatically associating the parent model and the tenant. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param \Sprout\Contracts\Tenancy $tenancy + * + * @return void + * + * @phpstan-param ParentModel $model + */ + public function populateRelation(ParentModel $model, Tenancy $tenancy): void + { + // If we don't have a tenant, or the relationship is already loaded, + // we can skip this + if (! $tenancy->check() || $model->relationLoaded($this->getRelationName())) { + return; + } + + /** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation */ + $relation = $this->getRelation($model); + + /** + * @var \Sprout\Contracts\Tenant $tenant + * @phpstan-var ChildModel $tenant + */ + $tenant = $tenancy->tenant(); + + // The assumption is that this method is only ever called on 'created', + // so it's safe to assume there are no other tenants + $relation->attach($tenant); + $model->setRelation($this->getRelationName(), $tenant->newCollection([$tenant])); + } + + /** + * Hydrate the tenant relationship + * + * This method sets the relation on the parent model to be the current + * tenant if it belongs to it. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param \Sprout\Contracts\Tenancy $tenancy + * + * @return void + * + * @phpstan-param ParentModel $model + */ + public function hydrateRelation(ParentModel $model, Tenancy $tenancy): void + { + // If we don't have a tenant, or the relationship is already loaded, + // we can skip this + if (! $tenancy->check()) { + return; + } + + /** + * @var \Sprout\Contracts\Tenant $tenant + * @psalm-var ChildModel $tenant + */ + $tenant = $tenancy->tenant(); + + if ($model->relationLoaded($this->getRelationName())) { + /** @var \Illuminate\Database\Eloquent\Collection|null $models */ + $models = $model->getRelation($this->getRelationName()); + + if ($models !== null && $models->contains(function (Model $model) use ($tenant) { + return $model->is($tenant); + })) { + return; + } + } + + // If the tenancy is configured to do a hydration check, we'll need to run + // a query to see if this model IS related to the tenant + if ($tenancy->option('hydration.check', true)) { + // Run the query + $exists = $model->newQuery()->whereHas($this->getRelationName(), function (Builder $query) use ($tenant) { + $query->whereKey($tenant->getTenantKey()); + })->exists(); + + // The model isn't related to the tenant + if (! $exists) { + // If the hydration is set to be strict for the current tenancy, + // we'll need an exception + if ($tenancy->option('hydration.strict', false)) { + // TODO: Abstract out to specific exception + throw new RuntimeException( + 'Child model [' . $model::class . '::' . $model->getKey() + . '] is not related to the tenant [' . $tenant->getTenantKey() + . '] for tenancy [' . $tenancy->getName() . ']' + ); + } + + return; + } + } + + // If we're hitting here we're either not doing a check, or the check + // succeeded, so we'll set the relation + $model->setRelation( + $this->getRelationName(), + // Make sure to create a proper collection, just in case there's + // a special one + ($models ?? $tenant->newCollection())->add($tenant) + ); + } +}