php-tenant/Concerns/HasPageRelationships.php
Claude 275cba29d7
refactor: guard external relationships in User model with class_exists()
Moves all external module relationships (Page, Project, Domain, Pixel,
AnalyticsWebsite, AnalyticsGoal, PushWebsite, PushCampaign, PushSegment,
PushFlow, TrustCampaign, TrustNotification, Order) out of the User model
into dedicated opt-in traits that resolve model classes at runtime via
class_exists(). This prevents fatal errors when consuming apps do not
have those modules installed.

New traits:
- HasPageRelationships (pages, projects, domains, pixels, bio/sub-page entitlements)
- HasAnalyticsRelationships (analytics websites, goals)
- HasPushRelationships (push websites, campaigns, segments, flows)
- HasTrustRelationships (trust campaigns, notifications)
- HasOrderRelationships (orders, vanity URL entitlement)

The User model still uses all five traits so existing consumers see no
breaking changes — the relationships now return null instead of throwing
a class-not-found error when the backing module is absent.

Fixes #6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:22:36 +00:00

237 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Tenant\Concerns;
use Core\Tenant\Models\Boost;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Page-related relationships for the User model.
*
* Requires the Page module (Core\Mod\Social or equivalent) to be installed.
* All relationships are guarded with class_exists() so the tenant package
* remains functional even when the Page module is absent.
*
* @mixin \Core\Tenant\Models\User
*/
trait HasPageRelationships
{
/**
* Get all pages owned by this user.
*/
public function pages(): ?HasMany
{
$class = $this->resolvePageModel();
if (! $class) {
return null;
}
return $this->hasMany($class);
}
/**
* Get all page projects (folders) owned by this user.
*/
public function pageProjects(): ?HasMany
{
$class = $this->resolvePageProjectModel();
if (! $class) {
return null;
}
return $this->hasMany($class);
}
/**
* Get all custom domains owned by this user.
*/
public function pageDomains(): ?HasMany
{
$class = $this->resolvePageDomainModel();
if (! $class) {
return null;
}
return $this->hasMany($class);
}
/**
* Get all tracking pixels owned by this user.
*/
public function pagePixels(): ?HasMany
{
$class = $this->resolvePagePixelModel();
if (! $class) {
return null;
}
return $this->hasMany($class);
}
/**
* Get the user's bio.pages entitlement (base + boosts).
*/
public function getBioPagesLimit(): int
{
// Base: 1 page for all tiers
$base = 1;
// Add from boosts
$boostPages = $this->boosts()
->where('feature_code', 'bio.pages')
->where('status', Boost::STATUS_ACTIVE)
->sum('limit_value');
return $base + (int) $boostPages;
}
/**
* Check if user can create more bio pages.
*/
public function canCreateBioPage(): bool
{
$pages = $this->pages();
if (! $pages) {
return false;
}
return $pages->rootPages()->count() < $this->getBioPagesLimit();
}
/**
* Get remaining bio page slots.
*/
public function remainingBioPageSlots(): int
{
$pages = $this->pages();
if (! $pages) {
return 0;
}
return max(0, $this->getBioPagesLimit() - $pages->rootPages()->count());
}
/**
* Get the user's sub-page limit (0 base + boosts).
*/
public function getSubPagesLimit(): int
{
// Base: 0 sub-pages (free tier)
$base = 0;
// Add from boosts
$boostPages = $this->boosts()
->where('feature_code', 'webpage.sub_pages')
->where('status', Boost::STATUS_ACTIVE)
->sum('limit_value');
return $base + (int) $boostPages;
}
/**
* Get the total sub-pages count across all root pages.
*/
public function getSubPagesCount(): int
{
$pages = $this->pages();
if (! $pages) {
return 0;
}
return $pages->subPages()->count();
}
/**
* Check if user can create more sub-pages.
*/
public function canCreateSubPage(): bool
{
return $this->getSubPagesCount() < $this->getSubPagesLimit();
}
/**
* Get remaining sub-page slots.
*/
public function remainingSubPageSlots(): int
{
return max(0, $this->getSubPagesLimit() - $this->getSubPagesCount());
}
/**
* Resolve the Page model class, checking common namespaces.
*/
protected function resolvePageModel(): ?string
{
foreach ($this->getPageModelCandidates('Page') as $candidate) {
if (class_exists($candidate)) {
return $candidate;
}
}
return null;
}
/**
* Resolve the Project model class, checking common namespaces.
*/
protected function resolvePageProjectModel(): ?string
{
foreach ($this->getPageModelCandidates('Project') as $candidate) {
if (class_exists($candidate)) {
return $candidate;
}
}
return null;
}
/**
* Resolve the Domain model class, checking common namespaces.
*/
protected function resolvePageDomainModel(): ?string
{
foreach ($this->getPageModelCandidates('Domain') as $candidate) {
if (class_exists($candidate)) {
return $candidate;
}
}
return null;
}
/**
* Resolve the Pixel model class, checking common namespaces.
*/
protected function resolvePagePixelModel(): ?string
{
foreach ($this->getPageModelCandidates('Pixel') as $candidate) {
if (class_exists($candidate)) {
return $candidate;
}
}
return null;
}
/**
* Get candidate class names for a Page module model.
*
* @return list<string>
*/
protected function getPageModelCandidates(string $model): array
{
return [
"Core\\Mod\\Social\\Models\\{$model}",
"App\\Models\\{$model}",
];
}
}