diff --git a/app/Http/Controllers/Forms/FormController.php b/app/Http/Controllers/Forms/FormController.php index 2a0fc0c..9e0d85c 100644 --- a/app/Http/Controllers/Forms/FormController.php +++ b/app/Http/Controllers/Forms/FormController.php @@ -34,7 +34,7 @@ class FormController extends Controller $this->authorize('viewAny', Form::class); $workspaceIsPro = $workspace->is_pro; - $forms = $workspace->forms()->with(['creator','views','submissions']) + $forms = $workspace->forms() ->orderByDesc('updated_at') ->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){ @@ -66,7 +66,7 @@ class FormController extends Controller $this->authorize('viewAny', Form::class); $workspaceIsPro = $workspace->is_pro; - $newForms = $workspace->forms()->with(['creator','views','submissions'])->get()->map(function (Form $form) use ($workspace, $workspaceIsPro){ + $newForms = $workspace->forms()->get()->map(function (Form $form) use ($workspace, $workspaceIsPro){ // Add attributes for faster loading $form->extra = (object) [ 'loadedWorkspace' => $workspace, diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php index 7f6a86f..34c04b1 100644 --- a/app/Http/Resources/FormResource.php +++ b/app/Http/Resources/FormResource.php @@ -25,7 +25,6 @@ class FormResource extends JsonResource } $ownerData = $this->userIsFormOwner() ? [ - 'creator' => new UserResource($this->creator), 'views_count' => $this->views_count, 'submissions_count' => $this->submissions_count, 'notifies' => $this->notifies, diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php index 497a1e3..e76f399 100644 --- a/app/Models/Forms/Form.php +++ b/app/Models/Forms/Form.php @@ -4,6 +4,8 @@ namespace App\Models\Forms; use App\Events\Models\FormCreated; use App\Models\Integration\FormZapierWebhook; +use App\Models\Traits\CachableAttributes; +use App\Models\Traits\CachesAttributes; use App\Models\User; use App\Models\Workspace; use Database\Factories\FormFactory; @@ -17,8 +19,9 @@ use Stevebauman\Purify\Facades\Purify; use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Casts\Attribute; -class Form extends Model +class Form extends Model implements CachableAttributes { + use CachesAttributes; const DARK_MODE_VALUES = ['auto', 'light', 'dark']; const THEMES = ['default', 'simple', 'notion']; const WIDTHS = ['centered', 'full']; @@ -126,6 +129,12 @@ class Form extends Model 'removed_properties' ]; + protected $cachableAttributes = [ + 'is_pro', + 'submissions_count', + 'views_count', + ]; + /** * The event map for the model. * @@ -137,7 +146,9 @@ class Form extends Model public function getIsProAttribute() { - return optional($this->workspace)->is_pro; + return $this->remember('is_pro', 15, function(): bool { + return optional($this->workspace)->is_pro; + }); } public function getShareUrlAttribute() @@ -155,19 +166,21 @@ class Form extends Model public function getSubmissionsCountAttribute() { - return $this->submissions()->count(); + return $this->remember('submissions_count', 5, function(): int { + return $this->submissions()->count(); + }); } public function getViewsCountAttribute() { - if (env('DB_CONNECTION') == 'pgsql') { + return $this->remember('views_count', 5, function(): int { + if (env('DB_CONNECTION') == 'mysql') { + return (int)($this->views()->count() + + $this->statistics()->sum(DB::raw("json_extract(data, '$.views')"))); + } return $this->views()->count() + $this->statistics()->sum(DB::raw("cast(data->>'views' as integer)")); - } elseif (env('DB_CONNECTION') == 'mysql') { - return (int)($this->views()->count() + - $this->statistics()->sum(DB::raw("json_extract(data, '$.views')"))); - } - return 0; + }); } public function setDescriptionAttribute($value) @@ -205,6 +218,7 @@ class Form extends Model public function getMaxNumberOfSubmissionsReachedAttribute() { + $this->disableCache('submissions_count'); return ($this->max_submissions_count && $this->max_submissions_count <= $this->submissions_count); } diff --git a/app/Models/Traits/CachableAttributes.php b/app/Models/Traits/CachableAttributes.php new file mode 100644 index 0000000..d012505 --- /dev/null +++ b/app/Models/Traits/CachableAttributes.php @@ -0,0 +1,45 @@ + */ + protected $attributeCache = []; + + protected $disabledCache = []; + + public static function bootCachesAttributes(): void + { + static::deleting(function (Model $model): void { + /** @var Model|CachableAttributes $model */ + $model->flush(); + }); + } + + public function disableCache($key) + { + $this->disabledCache[] = $key; + return $this; + } + + public function remember(string $attribute, ?int $ttl, Closure $callback) + { + if (in_array($attribute, $this->disabledCache)) { + return value($callback); + } + + if ($ttl === 0 || ! $this->exists) { + if (! isset($this->attributeCache[$attribute])) { + $this->attributeCache[$attribute] = value($callback); + } + + return $this->attributeCache[$attribute]; + } + + if ($ttl === null) { + return $this->getCacheRepository()->rememberForever($this->getCacheKey($attribute), $callback); + } + + if ($ttl < 0) { + throw new InvalidArgumentException("The TTL has to be null, 0 or any positive number - you provided `{$ttl}`."); + } + + return $this->getCacheRepository()->remember($this->getCacheKey($attribute), $ttl, $callback); + } + + public function rememberForever(string $attribute, Closure $callback) + { + return $this->remember($attribute, null, $callback); + } + + public function forget(string $attribute): bool + { + unset($this->attributeCache[$attribute]); + + if (! $this->exists) { + return true; + } + + return $this->getCacheRepository()->forget($this->getCacheKey($attribute)); + } + + public function flush(): bool + { + $result = true; + + foreach ($this->cachableAttributes ?? [] as $attribute) { + $result = $this->forget($attribute) ? $result : false; + } + + return $result; + } + + protected function getCacheKey(string $attribute): string + { + return implode('.', [ + $this->attributeCachePrefix ?? 'model_attribute_cache', + $this->getConnectionName() ?? 'connection', + $this->getTable(), + $this->getKey(), + $attribute, + $this->updated_at?->timestamp ?? '0' + ]); + } + + protected function getCacheRepository(): CacheRepository + { + return $this->getCacheFactory()->store($this->attributeCacheStore); + } + + protected function getCacheFactory(): CacheFactoryContract + { + return app('cache'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 764d927..a7c3127 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -59,13 +59,11 @@ class User extends Authenticatable implements JWTSubject 'photo_url', ]; - protected $withCount = ['workspaces']; - public function ownsForm(Form $form) { return $this->workspaces()->find($form->workspace_id) !== null; } - + /** * Get the profile photo URL attribute. * diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index 262633a..8bf903b 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -4,12 +4,14 @@ namespace App\Models; use App\Http\Requests\AnswerFormRequest; use App\Models\Forms\Form; +use App\Models\Traits\CachableAttributes; +use App\Models\Traits\CachesAttributes; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -class Workspace extends Model +class Workspace extends Model implements CachableAttributes { - use HasFactory; + use HasFactory, CachesAttributes; const MAX_FILE_SIZE_FREE = 5000000; // 5 MB const MAX_FILE_SIZE_PRO = 50000000; // 50 MB @@ -32,20 +34,14 @@ class Workspace extends Model 'custom_domains' => 'array', ]; - public function getIsProAttribute() - { - if(is_null(config('cashier.key'))){ - return true; // If no paid plan so TRUE for ALL - } - - // Make sure at least one owner is pro - foreach ($this->owners as $owner) { - if ($owner->is_subscribed) { - return true; - } - } - return false; - } + protected $cachableAttributes = [ + 'is_pro', + 'is_enterprise', + 'is_risky', + 'submissions_count', + 'max_file_size', + 'custom_domain_count' + ]; public function getMaxFileSizeAttribute() { @@ -53,18 +49,20 @@ class Workspace extends Model return self::MAX_FILE_SIZE_PRO; } - // Return max file size depending on subscription - foreach ($this->owners as $owner) { - if ($owner->is_subscribed) { - if ($license = $owner->activeLicense()) { - // In case of special License - return $license->max_file_size; + return $this->remember('max_file_size', 15, function(): int { + // Return max file size depending on subscription + foreach ($this->owners as $owner) { + if ($owner->is_subscribed) { + if ($license = $owner->activeLicense()) { + // In case of special License + return $license->max_file_size; + } } + return self::MAX_FILE_SIZE_PRO; } - return self::MAX_FILE_SIZE_PRO; - } - return self::MAX_FILE_SIZE_FREE; + return self::MAX_FILE_SIZE_FREE; + }); } public function getCustomDomainCountLimitAttribute() @@ -73,18 +71,36 @@ class Workspace extends Model return null; } - // Return max file size depending on subscription - foreach ($this->owners as $owner) { - if ($owner->is_subscribed) { - if ($license = $owner->activeLicense()) { - // In case of special License - return $license->custom_domain_limit_count; + return $this->remember('custom_domain_count', 15, function(): int { + foreach ($this->owners as $owner) { + if ($owner->is_subscribed) { + if ($license = $owner->activeLicense()) { + // In case of special License + return $license->custom_domain_limit_count; + } } + return self::MAX_DOMAIN_PRO; } - return self::MAX_DOMAIN_PRO; + + return 0; + }); + } + + public function getIsProAttribute() + { + if(is_null(config('cashier.key'))){ + return true; // If no paid plan so TRUE for ALL } - return 0; + return $this->remember('is_pro', 15, function(): bool { + // Make sure at least one owner is pro + foreach ($this->owners as $owner) { + if ($owner->is_subscribed) { + return true; + } + } + return false; + }); } public function getIsEnterpriseAttribute() @@ -93,34 +109,41 @@ class Workspace extends Model return true; // If no paid plan so TRUE for ALL } - foreach ($this->owners as $owner) { - if ($owner->has_enterprise_subscription) { - return true; + return $this->remember('is_enterprise', 15, function(): bool { + // Make sure at least one owner is pro + foreach ($this->owners as $owner) { + if ($owner->has_enterprise_subscription) { + return true; + } } - } - return false; + return false; + }); } public function getIsRiskyAttribute() { - // A workspace is risky if all of his users are risky - foreach ($this->owners as $owner) { - if (!$owner->is_risky) { - return false; + return $this->remember('is_risky', 15, function(): bool { + // A workspace is risky if all of his users are risky + foreach ($this->owners as $owner) { + if (!$owner->is_risky) { + return false; + } } - } - return true; + return true; + }); } public function getSubmissionsCountAttribute() { - $total = 0; - foreach ($this->forms as $form) { - $total += $form->submissions_count; - } + return $this->remember('submissions_count', 15, function(): int { + $total = 0; + foreach ($this->forms as $form) { + $total += $form->submissions_count; + } - return $total; + return $total; + }); } /** diff --git a/app/Policies/WorkspacePolicy.php b/app/Policies/WorkspacePolicy.php index e97a25e..4a237d8 100644 --- a/app/Policies/WorkspacePolicy.php +++ b/app/Policies/WorkspacePolicy.php @@ -30,7 +30,7 @@ class WorkspacePolicy */ public function view(User $user, Workspace $workspace) { - return $user->workspaces()->find($workspace->id)!==null; + return $workspace->users()->find($user->id)!==null; } /** diff --git a/resources/js/components/Navbar.vue b/resources/js/components/Navbar.vue index a8dadb7..40a2c6c 100644 --- a/resources/js/components/Navbar.vue +++ b/resources/js/components/Navbar.vue @@ -10,34 +10,37 @@ > {{ appName }} - + - +