Add @property, @property-read, @method, and @mixin PHPDoc annotations to the seven core Eloquent models for IDE autocompletion support. Models annotated: - Workspace: all columns, relationships, scopes (active, ordered) - User: all columns, relationships, factory - WorkspaceMember: relationship props, scope methods (forWorkspace, forUser, withRole, inTeam, owners) - WorkspaceInvitation: all columns, relationships, scopes (pending, expired, accepted) - Namespace_: all columns, relationships, scopes (active, ordered, ownedByUser, ownedByWorkspace, accessibleBy) - Package: all columns, relationships, scopes (active, public, base, addons, purchasable, free, ordered) - Feature: all columns, relationships, scopes (active, inCategory, root) Fixes #31 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
351 lines
11 KiB
PHP
351 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Tenant\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Namespace model - universal tenant boundary for products.
|
|
*
|
|
* A namespace provides a clean ownership boundary where products belong to
|
|
* a namespace rather than directly to User/Workspace. The namespace itself
|
|
* has polymorphic ownership (User or Workspace can own).
|
|
*
|
|
* @property int $id
|
|
* @property string $uuid
|
|
* @property string $name
|
|
* @property string $slug
|
|
* @property string|null $description
|
|
* @property string $icon
|
|
* @property string $color
|
|
* @property string $owner_type
|
|
* @property int $owner_id
|
|
* @property int|null $workspace_id
|
|
* @property array|null $settings
|
|
* @property bool $is_default
|
|
* @property bool $is_active
|
|
* @property int $sort_order
|
|
* @property \Illuminate\Support\Carbon|null $created_at
|
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
|
* @property-read User|Workspace $owner
|
|
* @property-read Workspace|null $workspace
|
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, NamespacePackage> $namespacePackages
|
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, Boost> $boosts
|
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, UsageRecord> $usageRecords
|
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, EntitlementLog> $entitlementLogs
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ active()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ ordered()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ ownedByUser(User|int $user)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ ownedByWorkspace(Workspace|int $workspace)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ accessibleBy(User $user)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ newModelQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ newQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Namespace_ query()
|
|
*
|
|
* @mixin \Eloquent
|
|
*/
|
|
class Namespace_ extends Model
|
|
{
|
|
use HasFactory;
|
|
use SoftDeletes;
|
|
|
|
/**
|
|
* The table associated with the model.
|
|
*/
|
|
protected $table = 'namespaces';
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*/
|
|
protected $fillable = [
|
|
'uuid',
|
|
'name',
|
|
'slug',
|
|
'description',
|
|
'icon',
|
|
'color',
|
|
'owner_type',
|
|
'owner_id',
|
|
'workspace_id',
|
|
'settings',
|
|
'is_default',
|
|
'is_active',
|
|
'sort_order',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast.
|
|
*/
|
|
protected $casts = [
|
|
'settings' => 'array',
|
|
'is_default' => 'boolean',
|
|
'is_active' => 'boolean',
|
|
'sort_order' => 'integer',
|
|
];
|
|
|
|
/**
|
|
* Boot the model.
|
|
*/
|
|
protected static function booted(): void
|
|
{
|
|
static::creating(function (self $namespace) {
|
|
if (empty($namespace->uuid)) {
|
|
$namespace->uuid = (string) Str::uuid();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Ownership Relationships
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get the owner of the namespace (User or Workspace).
|
|
*/
|
|
public function owner(): MorphTo
|
|
{
|
|
return $this->morphTo();
|
|
}
|
|
|
|
/**
|
|
* Get the workspace for billing aggregation (if set).
|
|
*
|
|
* This is separate from owner - a user-owned namespace can still
|
|
* have a workspace context for billing purposes.
|
|
*/
|
|
public function workspace(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Workspace::class);
|
|
}
|
|
|
|
/**
|
|
* Check if this namespace is owned by a user.
|
|
*/
|
|
public function isOwnedByUser(): bool
|
|
{
|
|
return $this->owner_type === User::class;
|
|
}
|
|
|
|
/**
|
|
* Check if this namespace is owned by a workspace.
|
|
*/
|
|
public function isOwnedByWorkspace(): bool
|
|
{
|
|
return $this->owner_type === Workspace::class;
|
|
}
|
|
|
|
/**
|
|
* Get the owner as User (or null if workspace-owned).
|
|
*/
|
|
public function getOwnerUser(): ?User
|
|
{
|
|
if ($this->isOwnedByUser()) {
|
|
return $this->owner;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the owner as Workspace (or null if user-owned).
|
|
*/
|
|
public function getOwnerWorkspace(): ?Workspace
|
|
{
|
|
if ($this->isOwnedByWorkspace()) {
|
|
return $this->owner;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Entitlement Relationships
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Active package assignments for this namespace.
|
|
*/
|
|
public function namespacePackages(): HasMany
|
|
{
|
|
return $this->hasMany(NamespacePackage::class);
|
|
}
|
|
|
|
/**
|
|
* Active boosts for this namespace.
|
|
*/
|
|
public function boosts(): HasMany
|
|
{
|
|
return $this->hasMany(Boost::class);
|
|
}
|
|
|
|
/**
|
|
* Usage records for this namespace.
|
|
*/
|
|
public function usageRecords(): HasMany
|
|
{
|
|
return $this->hasMany(UsageRecord::class);
|
|
}
|
|
|
|
/**
|
|
* Entitlement logs for this namespace.
|
|
*/
|
|
public function entitlementLogs(): HasMany
|
|
{
|
|
return $this->hasMany(EntitlementLog::class);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Settings & Configuration
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get a setting value from the settings JSON column.
|
|
*/
|
|
public function getSetting(string $key, mixed $default = null): mixed
|
|
{
|
|
return data_get($this->settings, $key, $default);
|
|
}
|
|
|
|
/**
|
|
* Set a setting value in the settings JSON column.
|
|
*/
|
|
public function setSetting(string $key, mixed $value): self
|
|
{
|
|
$settings = $this->settings ?? [];
|
|
data_set($settings, $key, $value);
|
|
$this->settings = $settings;
|
|
|
|
return $this;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Scopes
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Scope to only active namespaces.
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
/**
|
|
* Scope to order by sort order.
|
|
*/
|
|
public function scopeOrdered($query)
|
|
{
|
|
return $query->orderBy('sort_order');
|
|
}
|
|
|
|
/**
|
|
* Scope to namespaces owned by a specific user.
|
|
*/
|
|
public function scopeOwnedByUser($query, User|int $user)
|
|
{
|
|
$userId = $user instanceof User ? $user->id : $user;
|
|
|
|
return $query->where('owner_type', User::class)
|
|
->where('owner_id', $userId);
|
|
}
|
|
|
|
/**
|
|
* Scope to namespaces owned by a specific workspace.
|
|
*/
|
|
public function scopeOwnedByWorkspace($query, Workspace|int $workspace)
|
|
{
|
|
$workspaceId = $workspace instanceof Workspace ? $workspace->id : $workspace;
|
|
|
|
return $query->where('owner_type', Workspace::class)
|
|
->where('owner_id', $workspaceId);
|
|
}
|
|
|
|
/**
|
|
* Scope to namespaces accessible by a user (owned by user OR owned by user's workspaces).
|
|
*/
|
|
public function scopeAccessibleBy($query, User $user)
|
|
{
|
|
$workspaceIds = $user->workspaces()->pluck('workspaces.id');
|
|
|
|
return $query->where(function ($q) use ($user, $workspaceIds) {
|
|
// User-owned namespaces
|
|
$q->where(function ($q2) use ($user) {
|
|
$q2->where('owner_type', User::class)
|
|
->where('owner_id', $user->id);
|
|
});
|
|
|
|
// Workspace-owned namespaces (where user is a member)
|
|
if ($workspaceIds->isNotEmpty()) {
|
|
$q->orWhere(function ($q2) use ($workspaceIds) {
|
|
$q2->where('owner_type', Workspace::class)
|
|
->whereIn('owner_id', $workspaceIds);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Helper Methods
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Check if a user has access to this namespace.
|
|
*/
|
|
public function isAccessibleBy(User $user): bool
|
|
{
|
|
// User owns the namespace directly
|
|
if ($this->isOwnedByUser() && $this->owner_id === $user->id) {
|
|
return true;
|
|
}
|
|
|
|
// Workspace owns the namespace and user is a member
|
|
if ($this->isOwnedByWorkspace()) {
|
|
return $user->workspaces()->where('workspaces.id', $this->owner_id)->exists();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the billing context for this namespace.
|
|
*
|
|
* Returns workspace if set, otherwise falls back to owner's default workspace.
|
|
*/
|
|
public function getBillingContext(): ?Workspace
|
|
{
|
|
// Explicit workspace set for billing
|
|
if ($this->workspace_id) {
|
|
return $this->workspace;
|
|
}
|
|
|
|
// Workspace-owned: use the owner workspace
|
|
if ($this->isOwnedByWorkspace()) {
|
|
return $this->owner;
|
|
}
|
|
|
|
// User-owned: fall back to user's default workspace
|
|
if ($this->isOwnedByUser() && $this->owner) {
|
|
return $this->owner->defaultHostWorkspace();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the route key name for route model binding.
|
|
*/
|
|
public function getRouteKeyName(): string
|
|
{
|
|
return 'uuid';
|
|
}
|
|
}
|