php-tenant/Models/Namespace_.php
Claude 126d454bba
chore: add IDE helper annotations to Eloquent models
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>
2026-03-24 13:55:50 +00:00

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';
}
}