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>
237 lines
5.3 KiB
PHP
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}",
|
|
];
|
|
}
|
|
}
|