The page about the project has been revived.

This commit is contained in:
2024-04-18 19:41:31 +05:00
parent 63ea5dac92
commit 24e0cf0eea
42 changed files with 1126 additions and 6 deletions

View File

@@ -2,9 +2,16 @@
namespace App\Dto\Builder;
use App\Models\User;
final readonly class Project
{
public function __construct(
private ?bool $isPublic = null,
) { }
public function isPublic(): ?bool
{
return $this->isPublic;
}
}

View File

@@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace App\Enums;
use Illuminate\Cache\TaggedCache;
use Illuminate\Support\Facades\Cache;
enum CacheTag: string
{
case Project = 'project';
public function getCache(): TaggedCache
{
return Cache::tags($this->value);
}
}

View File

@@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Enums\Site;
use App\Models\Project;
use App\Models\ProjectLanguage;
enum ProjectSection
{
case Home;
public function url(Project $project, ?ProjectLanguage $language = null): string
{
$parameters = [];
$prefixProject = '';
if ($project->http_host === null) {
$prefixProject = 'project.';
$parameters['project'] = $project->code;
}
$prefixLanguage = '';
if ($language?->is_default === false) {
$parameters['language'] = $language->code;
$prefixLanguage = '-language';
}
return match ($this) {
self::Home => \route($prefixProject . 'home' . $prefixLanguage, $parameters),
};
}
}

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use App\Http\Controllers\Controller as BaseController;
use app\ServiceResults\Site\PagePossibleWithoutTranslation;
use Illuminate\View\View;
abstract class Controller extends BaseController
{
protected function viewPageWithoutTranslation(PagePossibleWithoutTranslation $result): View
{
return \view('site.page-without-translation', [
'project' => $result->getProject(),
'language' => $result->getLanguage(),
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use App\Enums\Site\ProjectSection;
use App\Services\Site\ProjectService;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ProjectsController extends Controller
{
public function __construct(
private readonly ProjectService $projectService,
) { }
public function index(Request $request, ?string $language = null): View
{
$user = $request->user();
if (\is_null($request->project)) {
if (!\is_null($language)) {
abort(404);
}
$with = ['storage'];
$result = $this->projectService->getProjects($user, $with);
if ($result->isError()) {
$this->errors($result);
}
return \view('site.projects.index', $result->getData());
}
$result = $this->projectService->getAboutByProject($request->project, $language, $request->user());
if ($result->isError()) {
$this->errors($result);
}
if ($result->isTranslation()) {
return $this->viewPageWithoutTranslation($result);
}
return \view('site.projects.about', $result->getData());
}
public function about(string $project, Request $request, ?string $language = null)
{
$result = $this->projectService->getAbout($project, $language, $request->user());
if ($result->isError()) {
$this->errors($result);
}
if ($result->isTranslation()) {
return $this->viewPageWithoutTranslation($result);
}
if ($result->getProject()->http_href !== null) {
$link = ProjectSection::Home->url($result->getProject(), $result->getLanguage());
return \redirect($result->getProject()->http_href . $link, 302);
}
return \view('site.projects.about', $result->getData());
}
}

View File

@@ -6,7 +6,7 @@ use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AdminPanel
final class AdminPanel
{
/**
* Handle an incoming request.

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use App\Enums\CacheTag;
use App\Repositories\ProjectRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
use Closure;
class Project
{
public function __construct(
private readonly ProjectRepository $projectRepository,
) { }
public function handle(Request $request, Closure $next): Response
{
$projectCode = $request->route()?->parameter('project');
if ($projectCode === null) {
abort(404);
}
$seconds = 3600;
$project = CacheTag::Project->getCache()->remember(self::class . $projectCode, $seconds, function () use ($projectCode) {
return $this->projectRepository->getProjectByCode($projectCode) ?? false;
});
if ($project === false) {
abort(404);
}
unset($request->route()->parameters['project']);
$routeName = $request->route()->getName();
if (
$routeName !== null
&& $project->http_host !== null
&& $project->http_host !== $request->getSchemeAndHttpHost()
) {
$routeName = Str::of($routeName)->replaceStart('project.', '')->toString();
$route = \route($routeName, $request->route()->parameters(), false);
return redirect($project->http_host . $route, 302);
}
$request->merge(['project' => $project]);
return $next($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use App\Enums\CacheTag;
use App\Repositories\ProjectRepository;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class ProjectDomain
{
public function __construct(
private readonly ProjectRepository $projectRepository,
) { }
public function handle(Request $request, Closure $next): Response
{
$httpHost = $request->getSchemeAndHttpHost();
$seconds = 3600;
$project = CacheTag::Project->getCache()->remember(self::class . $httpHost, $seconds, function () use ($httpHost) {
return $this->projectRepository->getProjectByHttpHost($httpHost) ?? false;
});
if ($project === false) {
$project = null;
}
$request->merge(['project' => $project]);
return $next($request);
}
}

View File

@@ -18,10 +18,12 @@ class StoreUpdateRequest extends FormRequest implements FormRequestDto
*/
public function rules(): array
{
$projectId = $this->project ?? null;
return [
'name' => ['required', 'string', 'max:255'],
'code' => ['required', 'string', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/i'],
'http_host' => ['nullable', 'string', 'max:255', new HttpHost()],
'code' => ['required', 'string', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/'],
'http_host' => ['nullable', 'string', 'max:255', new HttpHost(), 'unique:projects,http_host,' . $projectId],
'is_public' => ['required', 'boolean'],
'logo.file' => ['nullable', 'numeric', 'min:1'],
@@ -29,7 +31,7 @@ class StoreUpdateRequest extends FormRequest implements FormRequestDto
'languages.items.*.id' => ['nullable', 'numeric'],
'languages.items.*.title' => ['required', 'string', 'max:255'],
'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-zA-Z_]+$/i'],
'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-z_]+$/'],
'languages.items.*.sort' => ['required', 'numeric', 'min:-1000', 'max:1000'],
'languages.default' => ['required', 'numeric', function (string $attribute, mixed $value, \Closure $fail) {
$languages = $this->input('languages.items', []);

View File

@@ -12,6 +12,7 @@ use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
@@ -56,6 +57,9 @@ class AppServiceProvider extends ServiceProvider
Relation::enforceMorphMap(Morph::map());
Route::pattern('language', '[a-z_]+');
Route::pattern('project', '[a-z0-9_-]+');
$this->configureRateLimiting();
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);
}

View File

@@ -2,10 +2,20 @@
namespace App\Repositories;
use App\Models\Project;
use App\Models\ProjectLanguage;
final class ProjectLanguageRepository
{
public function getProjectLanguageByCodeOrDefault(Project $project, ?string $code): ?ProjectLanguage
{
if (\is_null($code)) {
return $project->languages()->where('is_default', true)->first();
}
return $project->languages()->where('code', $code)->first();
}
public function isExistsLanguageById(int $projectId, int $languageId): bool
{
return ProjectLanguage::query()

View File

@@ -3,10 +3,13 @@
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\Project;
use App\Services\ProjectLink\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Dto\Builder\ProjectLink as ProjectLinkBuilderDto;
use App\Models\ProjectLink;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Builder;
final readonly class ProjectLinkRepository
{
@@ -29,4 +32,11 @@ final readonly class ProjectLinkRepository
{
return ProjectLink::query()->where('id', $id)->first();
}
public function getLinksByProject(Project $project, int $languageId): Collection
{
return $project->links()->where(function (Builder $query) use ($languageId) {
$query->whereNull('language_id')->orWhere('language_id', $languageId);
})->get();
}
}

View File

@@ -26,6 +26,11 @@ final readonly class ProjectRepository
return $this->createSearchInstanceCommand->execute($query);
}
public function getProjectByHttpHost(string $httpHost): ?Project
{
return Project::query()->where('http_host', $httpHost)->first();
}
public function getProjectById(int $id): ?Project
{
return Project::query()->where('id', $id)->first();

View File

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace app\ServiceResults\Site;
use App\Models\Project;
use App\Models\ProjectLanguage;
use App\ServiceResults\ServiceResult;
final class PagePossibleWithoutTranslation extends ServiceResult
{
public function __construct(
private readonly Project $project,
private readonly ProjectLanguage $language,
private readonly array $data,
private readonly bool $isTranslation,
) { }
public function isTranslation(): bool
{
return $this->isTranslation;
}
public function getData(): array
{
return $this->data;
}
public function getProject(): Project
{
return $this->project;
}
public function getLanguage(): ProjectLanguage
{
return $this->language;
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Services\Admin;
use App\Dto\Builder\Project as ProjectBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Project\StoreUpdate;
use App\Enums\CacheTag;
use App\Enums\Morph;
use App\Models\Project;
use App\Models\ProjectLanguage;
@@ -14,6 +15,7 @@ use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\ClearCacheCommandHandler;
use App\Services\Project\ProjectCommandHandler;
use App\Services\ProjectLanguage\ModelSyncCommand;
use App\Services\Role\CreateAdminRoleForProjectCommand;
@@ -29,6 +31,7 @@ final class ProjectService extends Service
private readonly CreateAdminRoleForProjectCommand $createAdminRoleForProjectCommand,
private readonly ModelSyncCommand $languageModelSyncCommand,
private readonly StorageService $storageService,
private readonly ClearCacheCommandHandler $clearCacheCommandHandler,
) { }
public function index(ProjectBuilderDto $projectBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
@@ -130,6 +133,7 @@ final class ProjectService extends Service
return $project;
});
$this->clearCacheCommandHandler->byTag(CacheTag::Project);
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
@@ -172,6 +176,7 @@ final class ProjectService extends Service
return $project;
});
$this->clearCacheCommandHandler->byTag(CacheTag::Project);
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
@@ -196,6 +201,7 @@ final class ProjectService extends Service
DB::transaction(function () use ($project) {
$this->projectCommandHandler->handleDestroy($project);
});
$this->clearCacheCommandHandler->byTag(CacheTag::Project);
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Services;
use App\Enums\CacheTag;
final readonly class ClearCacheCommandHandler
{
public function byTag(CacheTag $tag)
{
$tag->getCache()->flush();
}
}

View File

@@ -10,6 +10,10 @@ final readonly class BuilderCommand
{
public function execute(Relation | Builder $query, ProjectBuilderDto $projectBuilderDto): Relation | Builder
{
if ($projectBuilderDto->isPublic() !== null) {
$query->where('is_public', $projectBuilderDto->isPublic());
}
return $query;
}
}

View File

@@ -2,12 +2,16 @@
namespace App\Services;
use App\Models\Project;
use App\Models\ProjectLanguage;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\Site\PagePossibleWithoutTranslation;
use App\ServiceResults\StoreUpdateResult;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
abstract class Service
{
@@ -56,6 +60,11 @@ abstract class Service
return new ServiceResultArray(data: $data);
}
final protected function resultSitePage(Project $project, ProjectLanguage $language, array $data, bool $isTranslation): PagePossibleWithoutTranslation
{
return new PagePossibleWithoutTranslation($project, $language, $data, $isTranslation);
}
final protected function error(int $code, string $message, array $errors = []): ServiceResultError
{
return new ServiceResultError(

View File

@@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace App\Services\Site;
use App\Models\Project;
use App\Models\User;
use App\Dto\Builder\Project as ProjectBuilderDto;
use App\Repositories\ProjectContentRepository;
use App\Repositories\ProjectLanguageRepository;
use App\Repositories\ProjectLinkRepository;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\Site\PagePossibleWithoutTranslation;
use App\Services\Service;
final class ProjectService extends Service
{
public function __construct(
private readonly ProjectRepository $projectRepository,
private readonly ProjectLanguageRepository $projectLanguageRepository,
private readonly ProjectContentRepository $projectContentRepository,
private readonly ProjectLinkRepository $projectLinkRepository,
) { }
public function getProjects(?User $user, array $with): ServiceResultError | ServiceResultArray
{
$isPublic = null;
if (\is_null($user) || $user->cannot('viewAny', Project::class)) {
$isPublic = true;
}
$projectBuilderDto = new ProjectBuilderDto(
isPublic: $isPublic,
);
$projects = $this->projectRepository->getProjects($projectBuilderDto, $with);
return $this->result([
'projects' => $projects->all(),
]);
}
public function getAbout(string $projectCode, ?string $languageCode, ?User $user = null): ServiceResultError | PagePossibleWithoutTranslation
{
$project = $this->projectRepository->getProjectByCode($projectCode);
if (\is_null($project)) {
return $this->errNotFound('Project not found');
}
return $this->getAboutByProject($project, $languageCode, $user);
}
public function getAboutByProject(Project $project, ?string $languageCode, ?User $user = null): ServiceResultError | PagePossibleWithoutTranslation
{
if (
$project->is_public === false
&& ( $user === null || $user->cannot('view', $project) )
) {
return $this->errFobidden(__('Access is denied'));
}
$language = $this->projectLanguageRepository->getProjectLanguageByCodeOrDefault($project, $languageCode);
if (!$language) {
return $this->errNotFound(__('Language not found'));
}
$data = [
'project' => $project,
'language' => $language,
'content' => $this->projectContentRepository->getContentByLanguageId($project->id, $language->id),
'links' => $this->projectLinkRepository->getLinksByProject($project, $language->id),
];
return $this->resultSitePage($project, $language, $data, \is_null($data['content']));
}
}

View File

@@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace app\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
final class HomeLayout extends Component
{
public function render(): View
{
return view('layout.home');
}
}

View File

@@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace app\View\Components\Site;
use App\Models\ProjectLanguage;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\View\Component;
use Illuminate\View\View;
final class ChooseLanguage extends Component
{
public function __construct(
private readonly ProjectLanguage $language,
private readonly Collection $languages,
) { }
public function render(): View
{
$link = Str::of( request()->getRequestUri() )->rtrim('/');
if ($link->endsWith('/language/' . $this->language->code)) {
$link = $link->replace('/language/' . $this->language->code, '', false);
}
return view('components.site.choose-language', [
'language' => $this->language,
'languages' => $this->languages,
'link' => (string) $link,
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace app\View\Components\Site;
use App\Enums\StorageType;
use App\Models\Project;
use App\Models\ProjectLanguage;
use Illuminate\View\Component;
use Illuminate\View\View;
final class Layout extends Component
{
public function __construct(
private readonly Project $project,
private readonly ProjectLanguage $language,
) { }
public function render(): View
{
return view('layout.site', [
'project' => $this->project,
'logo' => $this->project->getStorageOne(StorageType::Logo),
'language' => $this->language,
]);
}
}