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