diff --git a/app/Console/Commands/ClearRepositoryCache.php b/app/Console/Commands/ClearRepositoryCache.php new file mode 100644 index 0000000..b7280d7 --- /dev/null +++ b/app/Console/Commands/ClearRepositoryCache.php @@ -0,0 +1,44 @@ +argument('repository'); + + if (! $repository || $repository === 'all') { + if (! $this->option('force') && ! $this->confirm('Clear all repository cache?')) { + $this->info('Cache clearing cancelled.'); + + return Command::SUCCESS; + } + + $this->info('Clearing all repository cache...'); + $this->cacheService->clearAllRepositoryCache(); + $this->info('All repository cache cleared successfully.'); + } else { + $this->info("Clearing cache for {$repository} repository..."); + $this->cacheService->clearRepositoryCache($repository); + $this->info("{$repository} repository cache cleared successfully."); + } + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/WarmUpRepositoryCache.php b/app/Console/Commands/WarmUpRepositoryCache.php new file mode 100644 index 0000000..ac1dbbc --- /dev/null +++ b/app/Console/Commands/WarmUpRepositoryCache.php @@ -0,0 +1,61 @@ +error('Cache warming is disabled in configuration.'); + + return Command::FAILURE; + } + + if (! $this->option('force') && ! $this->confirm('Warm up repository cache?')) { + $this->info('Cache warm-up cancelled.'); + + return Command::SUCCESS; + } + + $this->info('Warming up repository cache...'); + $this->cacheService->warmUpCache(); + $this->info('Repository cache warmed up successfully.'); + + $stats = $this->cacheService->getCacheStats(); + $this->table(['Metric', 'Value'], [ + ['Memory Usage', $this->formatBytes($stats['memory_usage'])], + ['Peak Memory', $this->formatBytes($stats['peak_memory'])], + ['Cache Tags', implode(', ', $stats['tags'])], + ]); + + return Command::SUCCESS; + } + + protected function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $unitIndex = 0; + + while ($bytes >= 1024 && $unitIndex < count($units) - 1) { + $bytes /= 1024; + $unitIndex++; + } + + return round($bytes, 2).' '.$units[$unitIndex]; + } +} diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 0000000..1aa9a0a --- /dev/null +++ b/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,65 @@ +app->bind( + ReadRepositoryInterface::class.'\\Domain', + DomainReadRepository::class + ); + + $this->app->bind( + WriteRepositoryInterface::class.'\\Domain', + DomainWriteRepository::class + ); + + $this->app->singleton(DomainReadRepository::class, function ($app) { + return new DomainReadRepository($app->make(\App\Models\Domain::class), $app->make('cache')); + }); + + $this->app->singleton(DomainWriteRepository::class, function ($app) { + return new DomainWriteRepository($app->make(\App\Models\Domain::class), $app->make('cache')); + }); + + // Username Repositories + $this->app->bind( + ReadRepositoryInterface::class.'\\Username', + UsernameReadRepository::class + ); + + $this->app->bind( + WriteRepositoryInterface::class.'\\Username', + UsernameWriteRepository::class + ); + + $this->app->singleton(UsernameReadRepository::class, function ($app) { + return new UsernameReadRepository($app->make(\App\Models\Username::class), $app->make('cache')); + }); + + $this->app->singleton(UsernameWriteRepository::class, function ($app) { + return new UsernameWriteRepository($app->make(\App\Models\Username::class), $app->make('cache')); + }); + } + + public function provides(): array + { + return [ + DomainReadRepository::class, + DomainWriteRepository::class, + UsernameReadRepository::class, + UsernameWriteRepository::class, + ]; + } +} diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php new file mode 100644 index 0000000..0718450 --- /dev/null +++ b/app/Repositories/BaseRepository.php @@ -0,0 +1,335 @@ +model = $model; + $this->cache = $cache; + $this->query = $model->newQuery(); + $this->cacheTag = $this->getCacheTag(); + } + + abstract protected function getCacheTag(): string; + + protected function getCacheKey(string $method, array $parameters = []): string + { + $key = $this->cacheTag.':'.$method; + + if (! empty($parameters)) { + $key .= ':'.md5(serialize($parameters)); + } + + $key .= ':'.md5(serialize($this->withRelations)); + $key .= ':'.md5(serialize($this->withCountRelations)); + + return $key; + } + + protected function executeQuery(callable $callback, ?string $cacheKey = null, ?int $ttl = null) + { + if (! $this->cachingEnabled || $cacheKey === null) { + return $callback(); + } + + $ttl = $ttl ?? $this->cacheTtl; + + return $this->cache->tags([$this->cacheTag])->remember($cacheKey, $ttl, $callback); + } + + protected function resetQuery(): void + { + $this->query = $this->model->newQuery(); + $this->withRelations = []; + $this->withCountRelations = []; + } + + public function findById(int $id, array $columns = ['*']): ?Model + { + $cacheKey = $this->getCacheKey('find_by_id', ['id' => $id, 'columns' => $columns]); + + return $this->executeQuery(function () use ($id, $columns) { + $result = $this->query->find($id, $columns); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function findBy(string $column, mixed $value, array $columns = ['*']): ?Model + { + $cacheKey = $this->getCacheKey('find_by', [$column => $value, 'columns' => $columns]); + + return $this->executeQuery(function () use ($column, $value, $columns) { + $result = $this->query->where($column, $value)->first($columns); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function all(array $columns = ['*']): Collection + { + $cacheKey = $this->getCacheKey('all', ['columns' => $columns]); + + return $this->executeQuery(function () use ($columns) { + $result = $this->query->get($columns); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function get(array $columns = ['*']): Collection + { + $cacheKey = $this->getCacheKey('get', ['columns' => $columns]); + + return $this->executeQuery(function () use ($columns) { + $result = $this->query->get($columns); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function paginate(int $perPage = 15, array $columns = ['*'], string $pageName = 'page'): LengthAwarePaginator + { + $page = request()->input($pageName, 1); + $cacheKey = $this->getCacheKey('paginate', [ + 'per_page' => $perPage, + 'columns' => $columns, + 'page' => $page, + ]); + + return $this->executeQuery(function () use ($perPage, $columns, $pageName) { + $result = $this->query->paginate($perPage, $columns, $pageName); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); // 30 minutes for pagination + } + + public function whereIn(string $column, array $values, array $columns = ['*']): Collection + { + $cacheKey = $this->getCacheKey('where_in', [$column => $values, 'columns' => $columns]); + + return $this->executeQuery(function () use ($column, $values, $columns) { + $result = $this->query->whereIn($column, $values)->get($columns); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function where(string $column, mixed $operator, mixed $value = null): static + { + if (func_num_args() === 2) { + $this->query->where($column, $operator); + } else { + $this->query->where($column, $operator, $value); + } + + return $this; + } + + public function whereNull(string $column): static + { + $this->query->whereNull($column); + + return $this; + } + + public function whereNotNull(string $column): static + { + $this->query->whereNotNull($column); + + return $this; + } + + public function whereBetween(string $column, array $values): static + { + $this->query->whereBetween($column, $values); + + return $this; + } + + public function orderBy(string $column, string $direction = 'asc'): static + { + $this->query->orderBy($column, $direction); + + return $this; + } + + public function limit(int $limit): static + { + $this->query->limit($limit); + + return $this; + } + + public function with(array $relations): static + { + $this->withRelations = array_merge($this->withRelations, $relations); + $this->query->with($relations); + + return $this; + } + + public function withCount(array $relations): static + { + $this->withCountRelations = array_merge($this->withCountRelations, $relations); + $this->query->withCount($relations); + + return $this; + } + + public function exists(): bool + { + $cacheKey = $this->getCacheKey('exists'); + + return $this->executeQuery(function () { + $result = $this->query->exists(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 300); // 5 minutes for exists check + } + + public function count(): int + { + $cacheKey = $this->getCacheKey('count'); + + return $this->executeQuery(function () { + $result = $this->query->count(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 600); // 10 minutes for count + } + + public function first(array $columns = ['*']): ?Model + { + $cacheKey = $this->getCacheKey('first', ['columns' => $columns]); + + return $this->executeQuery(function () use ($columns) { + $result = $this->query->first($columns); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function firstWhere(string $column, mixed $operator, mixed $value = null): ?Model + { + $cacheKey = $this->getCacheKey('first_where', [$column => $operator, 'value' => $value]); + + return $this->executeQuery(function () use ($column, $operator, $value) { + if (func_num_args() === 2) { + $result = $this->query->firstWhere($column, $operator); + } else { + $result = $this->query->firstWhere($column, $operator, $value); + } + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function pluck(string $column, ?string $key = null): Collection + { + $cacheKey = $this->getCacheKey('pluck', ['column' => $column, 'key' => $key]); + + return $this->executeQuery(function () use ($column, $key) { + $result = $this->query->pluck($column, $key); + $this->resetQuery(); + + return $result; + }, $cacheKey); + } + + public function clearCache(): void + { + $this->cache->tags([$this->cacheTag])->flush(); + } + + public function clearCacheForModel(Model $model): void + { + $this->cache->tags([$this->cacheTag])->flush(); + } + + public function disableCaching(): static + { + $this->cachingEnabled = false; + + return $this; + } + + public function enableCaching(): static + { + $this->cachingEnabled = true; + + return $this; + } + + public function setCacheTtl(int $ttl): static + { + $this->cacheTtl = $ttl; + + return $this; + } + + protected function startTransaction(): void + { + DB::beginTransaction(); + } + + protected function commitTransaction(): void + { + DB::commit(); + } + + protected function rollbackTransaction(): void + { + DB::rollBack(); + } + + public function transaction(callable $callback): mixed + { + try { + $this->startTransaction(); + $result = $callback(); + $this->commitTransaction(); + + return $result; + } catch (\Throwable $e) { + $this->rollbackTransaction(); + throw $e; + } + } +} diff --git a/app/Repositories/Contracts/ReadRepositoryInterface.php b/app/Repositories/Contracts/ReadRepositoryInterface.php new file mode 100644 index 0000000..32af7fa --- /dev/null +++ b/app/Repositories/Contracts/ReadRepositoryInterface.php @@ -0,0 +1,52 @@ +getCacheKey('active'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); // 30 minutes + } + + public function findActiveByType(?DomainType $domainType = null, ?ProviderType $providerType = null): array + { + $params = [ + 'domain_type' => $domainType?->value, + 'provider_type' => $providerType?->value, + ]; + $cacheKey = $this->getCacheKey('active_by_type', $params); + + return $this->executeQuery(function () use ($domainType, $providerType) { + $query = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }); + + if ($domainType) { + $query->where('domain_type', $domainType->value); + } + + if ($providerType) { + $query->where('provider_type', $providerType->value); + } + + $result = $query->pluck('name')->toArray(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); // 30 minutes + } + + public function findActiveByProvider(ProviderType $providerType): Collection + { + $cacheKey = $this->getCacheKey('active_by_provider', ['provider' => $providerType->value]); + + return $this->executeQuery(function () use ($providerType) { + $result = $this->query + ->where('is_active', true) + ->where('provider_type', $providerType->value) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findExpiringSoon(int $days = 7): Collection + { + $cacheKey = $this->getCacheKey('expiring_soon', ['days' => $days]); + + return $this->executeQuery(function () use ($days) { + $result = $this->query + ->where('is_active', true) + ->whereNotNull('ends_at') + ->where('ends_at', '<=', now()->addDays($days)) + ->where('ends_at', '>', now()) + ->orderBy('ends_at', 'asc') + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 900); // 15 minutes + } + + public function findInactive(): Collection + { + $cacheKey = $this->getCacheKey('inactive'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', false) + ->orWhere(function ($query) { + $query->whereNotNull('starts_at') + ->where('starts_at', '>', now()); + }) + ->orWhere(function ($query) { + $query->whereNotNull('ends_at') + ->where('ends_at', '<', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findRecentlyUsed(int $hours = 24): Collection + { + $cacheKey = $this->getCacheKey('recently_used', ['hours' => $hours]); + + return $this->executeQuery(function () use ($hours) { + $result = $this->query + ->whereNotNull('last_used_at') + ->where('last_used_at', '>=', now()->subHours($hours)) + ->orderBy('last_used_at', 'desc') + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 900); // 15 minutes + } + + public function findByName(string $name): ?Domain + { + $cacheKey = $this->getCacheKey('find_by_name', ['name' => $name]); + + return $this->executeQuery(function () use ($name) { + $result = $this->query->where('name', $name)->first(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 3600); // 1 hour + } + + public function countActive(): int + { + $cacheKey = $this->getCacheKey('count_active'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->count(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 600); // 10 minutes + } + + public function countByProvider(): array + { + $cacheKey = $this->getCacheKey('count_by_provider'); + + return $this->executeQuery(function () { + $result = $this->query + ->selectRaw('provider_type, COUNT(*) as count') + ->groupBy('provider_type') + ->pluck('count', 'provider_type') + ->toArray(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function countByType(): array + { + $cacheKey = $this->getCacheKey('count_by_type'); + + return $this->executeQuery(function () { + $result = $this->query + ->selectRaw('domain_type, COUNT(*) as count') + ->groupBy('domain_type') + ->pluck('count', 'domain_type') + ->toArray(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } +} diff --git a/app/Repositories/Domain/Write/DomainWriteRepository.php b/app/Repositories/Domain/Write/DomainWriteRepository.php new file mode 100644 index 0000000..339416f --- /dev/null +++ b/app/Repositories/Domain/Write/DomainWriteRepository.php @@ -0,0 +1,103 @@ +is_active = true; + $result = $domain->save(); + $this->clearRelatedCache($domain); + + return $result; + } + + public function deactivateDomain(Domain $domain): bool + { + $domain->is_active = false; + $result = $domain->save(); + $this->clearRelatedCache($domain); + + return $result; + } + + public function updateUsage(Domain $domain): bool + { + $domain->last_used_at = now(); + $domain->checked_at = now(); + $result = $domain->save(); + $this->clearRelatedCache($domain); + + return $result; + } + + public function updateCheckedAt(Domain $domain): bool + { + $domain->checked_at = now(); + $result = $domain->save(); + $this->clearRelatedCache($domain); + + return $result; + } + + public function setExpiration(Domain $domain, ?\DateTime $endsAt = null): bool + { + $domain->ends_at = $endsAt; + $result = $domain->save(); + $this->clearRelatedCache($domain); + + return $result; + } + + public function bulkActivate(array $domainIds): int + { + $updated = Domain::whereIn('id', $domainIds)->update(['is_active' => true]); + $this->clearCache(); + + return $updated; + } + + public function bulkDeactivate(array $domainIds): int + { + $updated = Domain::whereIn('id', $domainIds)->update(['is_active' => false]); + $this->clearCache(); + + return $updated; + } + + public function updateDailyMailboxLimit(Domain $domain, int $limit): bool + { + $domain->daily_mailbox_limit = $limit; + $result = $domain->save(); + $this->clearRelatedCache($domain); + + return $result; + } + + public function createWithDefaults(array $data): Domain + { + $defaults = [ + 'is_active' => true, + 'daily_mailbox_limit' => 100, + 'checked_at' => now(), + ]; + + $domainData = array_merge($defaults, $data); + + return $this->create($domainData); + } +} diff --git a/app/Repositories/Username/Read/UsernameReadRepository.php b/app/Repositories/Username/Read/UsernameReadRepository.php new file mode 100644 index 0000000..b9b6e77 --- /dev/null +++ b/app/Repositories/Username/Read/UsernameReadRepository.php @@ -0,0 +1,317 @@ +getCacheKey('active'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); // 30 minutes + } + + public function findActiveByType(?UsernameType $usernameType = null, ?ProviderType $providerType = null): array + { + $params = [ + 'username_type' => $usernameType?->value, + 'provider_type' => $providerType?->value, + ]; + $cacheKey = $this->getCacheKey('active_by_type', $params); + + return $this->executeQuery(function () use ($usernameType, $providerType) { + $query = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }); + + if ($usernameType) { + $query->where('username_type', $usernameType->value); + } + + if ($providerType) { + $query->where('provider_type', $providerType->value); + } + + $result = $query->pluck('username')->toArray(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); // 30 minutes + } + + public function findActiveByProvider(ProviderType $providerType): Collection + { + $cacheKey = $this->getCacheKey('active_by_provider', ['provider' => $providerType->value]); + + return $this->executeQuery(function () use ($providerType) { + $result = $this->query + ->where('is_active', true) + ->where('provider_type', $providerType->value) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findPublicActive(): Collection + { + $cacheKey = $this->getCacheKey('public_active'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where('username_type', UsernameType::PUBLIC->value) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findPremiumActive(): Collection + { + $cacheKey = $this->getCacheKey('premium_active'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where('username_type', UsernameType::PREMIUM->value) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findExpiringSoon(int $days = 7): Collection + { + $cacheKey = $this->getCacheKey('expiring_soon', ['days' => $days]); + + return $this->executeQuery(function () use ($days) { + $result = $this->query + ->where('is_active', true) + ->whereNotNull('ends_at') + ->where('ends_at', '<=', now()->addDays($days)) + ->where('ends_at', '>', now()) + ->orderBy('ends_at', 'asc') + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 900); // 15 minutes + } + + public function findInactive(): Collection + { + $cacheKey = $this->getCacheKey('inactive'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', false) + ->orWhere(function ($query) { + $query->whereNotNull('starts_at') + ->where('starts_at', '>', now()); + }) + ->orWhere(function ($query) { + $query->whereNotNull('ends_at') + ->where('ends_at', '<', now()); + }) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findRecentlyUsed(int $hours = 24): Collection + { + $cacheKey = $this->getCacheKey('recently_used', ['hours' => $hours]); + + return $this->executeQuery(function () use ($hours) { + $result = $this->query + ->whereNotNull('last_used_at') + ->where('last_used_at', '>=', now()->subHours($hours)) + ->orderBy('last_used_at', 'desc') + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 900); // 15 minutes + } + + public function findByUsername(string $username): ?Username + { + $cacheKey = $this->getCacheKey('find_by_username', ['username' => $username]); + + return $this->executeQuery(function () use ($username) { + $result = $this->query->where('username', $username)->first(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 3600); // 1 hour + } + + public function countActive(): int + { + $cacheKey = $this->getCacheKey('count_active'); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->count(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 600); // 10 minutes + } + + public function countByProvider(): array + { + $cacheKey = $this->getCacheKey('count_by_provider'); + + return $this->executeQuery(function () { + $result = $this->query + ->selectRaw('provider_type, COUNT(*) as count') + ->groupBy('provider_type') + ->pluck('count', 'provider_type') + ->toArray(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function countByType(): array + { + $cacheKey = $this->getCacheKey('count_by_type'); + + return $this->executeQuery(function () { + $result = $this->query + ->selectRaw('username_type, COUNT(*) as count') + ->groupBy('username_type') + ->pluck('count', 'username_type') + ->toArray(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } + + public function findAvailableForUse(UsernameType $type, int $limit = 10): Collection + { + $cacheKey = $this->getCacheKey('available_for_use', [ + 'type' => $type->value, + 'limit' => $limit, + ]); + + return $this->executeQuery(function () use ($type, $limit) { + $result = $this->query + ->where('is_active', true) + ->where('username_type', $type->value) + ->where(function ($query) { + $query->whereNull('starts_at') + ->orWhere('starts_at', '<=', now()); + }) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>=', now()); + }) + ->orderBy('last_used_at', 'asc') + ->limit($limit) + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 300); // 5 minutes + } + + public function findUnused(int $days = 30): Collection + { + $cacheKey = $this->getCacheKey('unused', ['days' => $days]); + + return $this->executeQuery(function () { + $result = $this->query + ->where('is_active', true) + ->where(function ($query) { + $query->whereNull('last_used_at') + ->orWhere('last_used_at', '<', now()->subDays($days)); + }) + ->orderBy('last_used_at', 'asc') + ->get(); + $this->resetQuery(); + + return $result; + }, $cacheKey, 1800); + } +} diff --git a/app/Repositories/Username/Write/UsernameWriteRepository.php b/app/Repositories/Username/Write/UsernameWriteRepository.php new file mode 100644 index 0000000..f51d8a5 --- /dev/null +++ b/app/Repositories/Username/Write/UsernameWriteRepository.php @@ -0,0 +1,163 @@ +is_active = true; + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function deactivateUsername(Username $username): bool + { + $username->is_active = false; + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function updateUsage(Username $username): bool + { + $username->last_used_at = now(); + $username->checked_at = now(); + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function updateCheckedAt(Username $username): bool + { + $username->checked_at = now(); + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function setExpiration(Username $username, ?\DateTime $endsAt = null): bool + { + $username->ends_at = $endsAt; + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function setStartDate(Username $username, ?\DateTime $startsAt = null): bool + { + $username->starts_at = $startsAt; + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function changeType(Username $username, UsernameType $type): bool + { + $username->username_type = $type->value; + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function bulkActivate(array $usernameIds): int + { + $updated = Username::whereIn('id', $usernameIds)->update(['is_active' => true]); + $this->clearCache(); + + return $updated; + } + + public function bulkDeactivate(array $usernameIds): int + { + $updated = Username::whereIn('id', $usernameIds)->update(['is_active' => false]); + $this->clearCache(); + + return $updated; + } + + public function updateDailyMailboxLimit(Username $username, int $limit): bool + { + $username->daily_mailbox_limit = $limit; + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function createWithDefaults(array $data): Username + { + $defaults = [ + 'is_active' => true, + 'daily_mailbox_limit' => 100, + 'checked_at' => now(), + ]; + + $usernameData = array_merge($defaults, $data); + + return $this->create($usernameData); + } + + public function markAsUsed(Username $username): bool + { + $username->last_used_at = now(); + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function resetUsage(Username $username): bool + { + $username->last_used_at = null; + $username->checked_at = now(); + $result = $username->save(); + $this->clearRelatedCache($username); + + return $result; + } + + public function bulkUpdateUsage(array $usernameIds): int + { + $updated = Username::whereIn('id', $usernameIds)->update([ + 'last_used_at' => now(), + 'checked_at' => now(), + ]); + $this->clearCache(); + + return $updated; + } + + public function bulkResetUsage(array $usernameIds): int + { + $updated = Username::whereIn('id', $usernameIds)->update([ + 'last_used_at' => null, + 'checked_at' => now(), + ]); + $this->clearCache(); + + return $updated; + } +} diff --git a/app/Repositories/WriteRepository.php b/app/Repositories/WriteRepository.php new file mode 100644 index 0000000..248aa51 --- /dev/null +++ b/app/Repositories/WriteRepository.php @@ -0,0 +1,187 @@ +model = $model; + $this->cache = $cache; + $this->cacheTag = $this->getCacheTag(); + $this->readRepositoryClass = $this->getReadRepositoryClass(); + } + + abstract protected function getCacheTag(): string; + + abstract protected function getReadRepositoryClass(): string; + + protected function getReadRepository(): mixed + { + return app($this->readRepositoryClass); + } + + protected function clearRelatedCache(Model $model): void + { + $this->getReadRepository()->clearCacheForModel($model); + } + + protected function clearCollectionCache(Collection $models): void + { + foreach ($models as $model) { + $this->clearRelatedCache($model); + } + } + + public function create(array $data): Model + { + $model = $this->model->create($data); + $this->clearRelatedCache($model); + + return $model; + } + + public function createMany(array $data): Collection + { + $models = $this->model->insert($data) ? $this->model->whereIn('id', + $this->model->latest()->take(count($data))->pluck('id') + )->get() : collect(); + + $this->getReadRepository()->clearCache(); + + return $models; + } + + public function update(Model $model, array $data): bool + { + $result = $model->update($data); + $this->clearRelatedCache($model); + + return $result; + } + + public function updateById(int $id, array $data): bool + { + $model = $this->model->findOrFail($id); + + return $this->update($model, $data); + } + + public function upsert(array $data, array|string $uniqueBy, ?array $update = null): Collection + { + $this->model->upsert($data, $uniqueBy, $update); + + // Clear all cache since upsert can affect multiple records + $this->getReadRepository()->clearCache(); + + // Return the upserted records + $uniqueValues = collect($data)->pluck($uniqueBy); + + return $this->model->whereIn($uniqueBy, $uniqueValues)->get(); + } + + public function delete(Model $model): bool + { + $result = $model->delete(); + $this->clearRelatedCache($model); + + return $result; + } + + public function deleteById(int $id): bool + { + $model = $this->model->findOrFail($id); + + return $this->delete($model); + } + + public function deleteMultiple(array $ids): int + { + $deleted = $this->model->whereIn('id', $ids)->delete(); + + // Clear all cache since multiple records are deleted + $this->getReadRepository()->clearCache(); + + return $deleted; + } + + public function restore(Model $model): bool + { + $result = $model->restore(); + $this->clearRelatedCache($model); + + return $result; + } + + public function restoreById(int $id): bool + { + $model = $this->model->withTrashed()->findOrFail($id); + + return $this->restore($model); + } + + public function forceDelete(Model $model): bool + { + $result = $model->forceDelete(); + $this->clearRelatedCache($model); + + return $result; + } + + public function forceDeleteById(int $id): bool + { + $model = $this->model->withTrashed()->findOrFail($id); + + return $this->forceDelete($model); + } + + public function sync(Model $model, string $relation, array $ids, bool $detaching = true): array + { + $result = $model->{$relation}()->sync($ids, $detaching); + $this->clearRelatedCache($model); + + return $result; + } + + public function attach(Model $model, string $relation, array $ids, array $attributes = [], bool $touch = true): void + { + $model->{$relation}()->attach($ids, $attributes, $touch); + $this->clearRelatedCache($model); + } + + public function detach(Model $model, string $relation, ?array $ids = null, bool $touch = true): int + { + $result = $model->{$relation}()->detach($ids, $touch); + $this->clearRelatedCache($model); + + return $result; + } + + public function clearCache(): void + { + $this->getReadRepository()->clearCache(); + } + + public function clearCacheForModel(Model $model): void + { + $this->clearRelatedCache($model); + } + + public function clearCacheForCollection(Collection $models): void + { + $this->clearCollectionCache($models); + } +} diff --git a/app/Services/CacheService.php b/app/Services/CacheService.php new file mode 100644 index 0000000..bd31dd9 --- /dev/null +++ b/app/Services/CacheService.php @@ -0,0 +1,127 @@ +getRepositoryTag($repository); + $this->cache->tags([$tag])->flush(); + } + + public function clearModelCache(string $model, ?int $modelId = null): void + { + $tag = $this->getModelTag($model); + + if ($modelId) { + // Clear specific model cache and related caches + $this->cache->tags([$tag])->flush(); + } else { + // Clear all cache for this model type + $this->cache->tags([$tag])->flush(); + } + } + + public function warmUpCache(): void + { + $config = config('repositories.cache.warming.commands', []); + + foreach ($config as $command) { + try { + $this->executeWarmUpCommand($command); + } catch (\Exception $e) { + \Log::warning("Failed to warm up cache for command: {$command}", [ + 'error' => $e->getMessage(), + ]); + } + } + } + + public function getCacheStats(): array + { + return [ + 'tags' => $this->getCacheTags(), + 'memory_usage' => memory_get_usage(true), + 'peak_memory' => memory_get_peak_usage(true), + ]; + } + + public function clearAllRepositoryCache(): void + { + $tags = collect([ + 'domains', + 'usernames', + ])->map(fn ($tag) => $this->getFullTag($tag)); + + $this->cache->tags($tags->toArray())->flush(); + } + + protected function getRepositoryTag(string $repository): string + { + return $this->getFullTag(config("repositories.cache.tags.{$repository}", $repository)); + } + + protected function getModelTag(string $model): string + { + $repository = strtolower(str_replace('App\\Models\\', '', $model)); + + return $this->getRepositoryTag($repository); + } + + protected function getFullTag(string $tag): string + { + $prefix = config('repositories.cache.prefix', 'repo'); + + return "{$prefix}:{$tag}"; + } + + protected function executeWarmUpCommand(string $command): void + { + match ($command) { + 'domains:active' => $this->warmUpActiveDomains(), + 'usernames:active' => $this->warmUpActiveUsernames(), + 'domains:count' => $this->warmUpDomainCounts(), + 'usernames:count' => $this->warmUpUsernameCounts(), + default => \Log::warning("Unknown warm-up command: {$command}"), + }; + } + + protected function warmUpActiveDomains(): void + { + app(\App\Repositories\Domain\Read\DomainReadRepository::class)->findActive(); + } + + protected function warmUpActiveUsernames(): void + { + app(\App\Repositories\Username\Read\UsernameReadRepository::class)->findActive(); + } + + protected function warmUpDomainCounts(): void + { + $repo = app(\App\Repositories\Domain\Read\DomainReadRepository::class); + $repo->countActive(); + $repo->countByProvider(); + $repo->countByType(); + } + + protected function warmUpUsernameCounts(): void + { + $repo = app(\App\Repositories\Username\Read\UsernameReadRepository::class); + $repo->countActive(); + $repo->countByProvider(); + $repo->countByType(); + } + + protected function getCacheTags(): Collection + { + return collect(config('repositories.cache.tags', []))->keys(); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 8843715..c589751 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,4 +3,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\Filament\DashPanelProvider::class, + App\Providers\RepositoryServiceProvider::class, ]; diff --git a/config/repositories.php b/config/repositories.php new file mode 100644 index 0000000..0fcd6d1 --- /dev/null +++ b/config/repositories.php @@ -0,0 +1,188 @@ + [ + /* + |-------------------------------------------------------------------------- + | Default Cache TTL (seconds) + |-------------------------------------------------------------------------- + | + | Default time-to-live for cached repository results. Can be overridden + | per repository or per method call. + | + */ + 'default_ttl' => env('REPOSITORY_CACHE_TTL', 3600), // 1 hour + + /* + |-------------------------------------------------------------------------- + | Enable Caching + |-------------------------------------------------------------------------- + | + | Global switch to enable/disable repository caching. Useful for + | debugging environments. + | + */ + 'enabled' => env('REPOSITORY_CACHE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Cache Prefix + |-------------------------------------------------------------------------- + | + | Prefix to use for all repository cache keys. + | + */ + 'prefix' => env('REPOSITORY_CACHE_PREFIX', 'repo'), + + /* + |-------------------------------------------------------------------------- + | Repository-specific TTL Configuration + |-------------------------------------------------------------------------- + | + | You can configure different TTL values for different repositories + | or specific methods. + | + */ + 'ttl' => [ + 'domains' => [ + 'default' => 3600, // 1 hour + 'active' => 1800, // 30 minutes + 'count' => 600, // 10 minutes + 'expiring_soon' => 900, // 15 minutes + 'recently_used' => 900, // 15 minutes + ], + 'usernames' => [ + 'default' => 3600, // 1 hour + 'active' => 1800, // 30 minutes + 'count' => 600, // 10 minutes + 'expiring_soon' => 900, // 15 minutes + 'recently_used' => 900, // 15 minutes + 'available_for_use' => 300, // 5 minutes + ], + ], + + /* + |-------------------------------------------------------------------------- + | Cache Tags + |-------------------------------------------------------------------------- + | + | Tags used for cache invalidation. Each repository should have its + | own tag to ensure selective cache clearing. + | + */ + 'tags' => [ + 'domains' => 'domains', + 'usernames' => 'usernames', + ], + + /* + |-------------------------------------------------------------------------- + | Cache Warming + |-------------------------------------------------------------------------- + | + | Configuration for warming up cache with frequently accessed data. + | + */ + 'warming' => [ + 'enabled' => env('REPOSITORY_CACHE_WARMING_ENABLED', false), + 'commands' => [ + 'domains:active', + 'usernames:active', + 'domains:count', + 'usernames:count', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Repository Configuration + |-------------------------------------------------------------------------- + | + | General repository configuration options. + | + */ + 'repositories' => [ + /* + |-------------------------------------------------------------------------- + | Default Page Size + |-------------------------------------------------------------------------- + | + | Default number of items per page for pagination. + | + */ + 'default_page_size' => 15, + + /* + |-------------------------------------------------------------------------- + | Max Page Size + |-------------------------------------------------------------------------- + | + | Maximum number of items allowed per page to prevent excessive + | memory usage. + | + */ + 'max_page_size' => 100, + + /* + |-------------------------------------------------------------------------- + | Transaction Timeout + |-------------------------------------------------------------------------- + | + | Timeout in seconds for repository transactions. + | + */ + 'transaction_timeout' => 30, + ], + + /* + |-------------------------------------------------------------------------- + | Performance Monitoring + |-------------------------------------------------------------------------- + | + | Configuration for monitoring repository performance and cache hit rates. + | + */ + 'monitoring' => [ + /* + |-------------------------------------------------------------------------- + | Enable Performance Monitoring + |-------------------------------------------------------------------------- + | + | Track query execution times and cache hit rates. + | + */ + 'enabled' => env('REPOSITORY_MONITORING_ENABLED', false), + + /* + |-------------------------------------------------------------------------- + | Log Slow Queries + |-------------------------------------------------------------------------- + | + | Log queries that take longer than this many milliseconds. + | + */ + 'slow_query_threshold' => 1000, // 1 second + + /* + |-------------------------------------------------------------------------- + | Log Cache Misses + |-------------------------------------------------------------------------- + | + | Log cache misses for debugging purposes. + | + */ + 'log_cache_misses' => env('REPOSITORY_LOG_CACHE_MISSES', false), + ], +];