From c26b6cc0ed74ef63959857db9cf240bdc1b8360a Mon Sep 17 00:00:00 2001 From: Anastas Mironov Date: Tue, 28 Mar 2023 17:28:59 +0600 Subject: [PATCH] feat: add articles API --- composer.json | 4 +- .../Article/Configs/admin-kit.php | 7 ++ .../Article/Data/Factories/ArticleFactory.php | 34 +++++++ .../ArticleSection/Article/Languages/ru.json | 4 +- .../ArticleSection/Article/Models/Article.php | 47 ++++++++- .../Article/Providers/MainServiceProvider.php | 10 ++ .../UI/API/Controllers/ArticleController.php | 18 +++- .../Article/UI/API/DTO/ArticleDTO.php | 39 ++++++++ .../UI/API/Repositories/ArticleInterface.php | 18 ++++ .../UI/API/Repositories/ArticleRepository.php | 95 +++++++++++++++++++ .../UI/API/Routes/GetArticle.v1.public.php | 6 -- .../UI/API/Routes/GetArticles.v1.public.php | 6 -- .../Article/UI/API/Routes/api.php | 10 ++ src/CoreServiceProvider.php | 2 + src/Repositories/AbstractRepository.php | 53 +++++++++++ src/Repositories/RepositoryInterface.php | 12 +++ 16 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 src/Containers/ArticleSection/Article/Configs/admin-kit.php create mode 100644 src/Containers/ArticleSection/Article/Data/Factories/ArticleFactory.php create mode 100644 src/Containers/ArticleSection/Article/UI/API/DTO/ArticleDTO.php create mode 100644 src/Containers/ArticleSection/Article/UI/API/Repositories/ArticleInterface.php create mode 100644 src/Containers/ArticleSection/Article/UI/API/Repositories/ArticleRepository.php delete mode 100644 src/Containers/ArticleSection/Article/UI/API/Routes/GetArticle.v1.public.php delete mode 100644 src/Containers/ArticleSection/Article/UI/API/Routes/GetArticles.v1.public.php create mode 100644 src/Containers/ArticleSection/Article/UI/API/Routes/api.php create mode 100644 src/Repositories/AbstractRepository.php create mode 100644 src/Repositories/RepositoryInterface.php diff --git a/composer.json b/composer.json index 8aa0282..299a17c 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,9 @@ "astrotomic/laravel-translatable": "^11.12", "cviebrock/eloquent-sluggable": "^9.0", "illuminate/contracts": "^9.0", - "orchid/platform": "^13.10" + "orchid/platform": "^13.10", + "spatie/laravel-data": "^3.2", + "spatie/laravel-query-builder": "^5.2" }, "require-dev": { "laravel/pint": "^1.0", diff --git a/src/Containers/ArticleSection/Article/Configs/admin-kit.php b/src/Containers/ArticleSection/Article/Configs/admin-kit.php new file mode 100644 index 0000000..d9b1e10 --- /dev/null +++ b/src/Containers/ArticleSection/Article/Configs/admin-kit.php @@ -0,0 +1,7 @@ + [ + 'enable_routes' => false, + ], +]; diff --git a/src/Containers/ArticleSection/Article/Data/Factories/ArticleFactory.php b/src/Containers/ArticleSection/Article/Data/Factories/ArticleFactory.php new file mode 100644 index 0000000..17f8bbf --- /dev/null +++ b/src/Containers/ArticleSection/Article/Data/Factories/ArticleFactory.php @@ -0,0 +1,34 @@ +mapWithKeys(fn ($value) => [ + $value => [ + 'title' => fake('ru')->realText(100), + 'content' => fake('ru')->randomHtml, + 'short_content' => fake('ru')->realText(500), + ], + ]) + ->toArray(); + + return [ + 'slug' => fake()->slug, + 'published_at' => fake()->boolean ? fake()->dateTimeThisMonth : null, + 'pinned' => fake()->boolean, + ...$translations, + ]; + } +} diff --git a/src/Containers/ArticleSection/Article/Languages/ru.json b/src/Containers/ArticleSection/Article/Languages/ru.json index b83a40c..b2c5923 100644 --- a/src/Containers/ArticleSection/Article/Languages/ru.json +++ b/src/Containers/ArticleSection/Article/Languages/ru.json @@ -8,5 +8,7 @@ "Pinned": "Закреплен", "Enter title...": "Введите заголовок...", - "Enter short content...": "Введите короткое содержание..." + "Enter short content...": "Введите короткое содержание...", + + "Article has not been published": "Новость не опубликована" } diff --git a/src/Containers/ArticleSection/Article/Models/Article.php b/src/Containers/ArticleSection/Article/Models/Article.php index d582874..f9e992d 100644 --- a/src/Containers/ArticleSection/Article/Models/Article.php +++ b/src/Containers/ArticleSection/Article/Models/Article.php @@ -8,6 +8,7 @@ use Astrotomic\Translatable\Translatable; use Carbon\Carbon; use Cviebrock\EloquentSluggable\Sluggable; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; @@ -19,9 +20,15 @@ use Orchid\Screen\AsSource; /** + * @property int $id * @property string $slug * @property bool $pinned * @property Carbon|null $published_at + * @property Carbon $created_at + * @property Carbon $updated_at + * @property string $title + * @property string $content + * @property string $short_content * @property Collection $image */ class Article extends Model implements TranslatableContract @@ -82,7 +89,6 @@ class Article extends Model implements TranslatableContract */ protected $allowedSorts = [ 'id', - 'title', 'published_at', ]; @@ -104,4 +110,43 @@ public function image(): MorphToMany { return $this->morphToMany(Attachment::class, 'attachmentable', 'attachmentable'); } + + public function scopeIsPublished(Builder $query): Builder + { + return $query->whereNotNull('published_at'); + } + + public function scopeIsTitleNotNull(Builder $query): Builder + { + return $query->whereHas('translation', function ($query) { + return $query->whereNotNull('title'); + }); + } + + public function scopeTitle(Builder $query, $search): Builder + { + return $query->whereHas('translation', function ($query) use ($search) { + $search = mb_strtolower($search); + + return $query->whereRaw('LOWER(title) LIKE (?)', ["%$search%"]); + }); + } + + public function scopeContent(Builder $query, $search): Builder + { + return $query->whereHas('translation', function ($query) use ($search) { + $search = mb_strtolower($search); + + return $query->whereRaw('LOWER(content) LIKE (?)', ["%$search%"]); + }); + } + + public function scopeShortContent(Builder $query, $search): Builder + { + return $query->whereHas('translation', function ($query) use ($search) { + $search = mb_strtolower($search); + + return $query->whereRaw('LOWER(short_content) LIKE (?)', ["%$search%"]); + }); + } } diff --git a/src/Containers/ArticleSection/Article/Providers/MainServiceProvider.php b/src/Containers/ArticleSection/Article/Providers/MainServiceProvider.php index 7e97c2f..b708be3 100644 --- a/src/Containers/ArticleSection/Article/Providers/MainServiceProvider.php +++ b/src/Containers/ArticleSection/Article/Providers/MainServiceProvider.php @@ -4,9 +4,19 @@ namespace AdminKit\Core\Containers\ArticleSection\Article\Providers; +use AdminKit\Core\Containers\ArticleSection\Article\UI\API\Repositories\ArticleInterface; +use AdminKit\Core\Containers\ArticleSection\Article\UI\API\Repositories\ArticleRepository; + class MainServiceProvider extends \AdminKit\Porto\Abstracts\Providers\MainServiceProvider { public array $serviceProviders = [ PlatformServiceProvider::class, ]; + + public function register(): void + { + $this->app->bind(ArticleInterface::class, ArticleRepository::class); + + parent::register(); + } } diff --git a/src/Containers/ArticleSection/Article/UI/API/Controllers/ArticleController.php b/src/Containers/ArticleSection/Article/UI/API/Controllers/ArticleController.php index ebf7224..9848bda 100644 --- a/src/Containers/ArticleSection/Article/UI/API/Controllers/ArticleController.php +++ b/src/Containers/ArticleSection/Article/UI/API/Controllers/ArticleController.php @@ -4,17 +4,27 @@ namespace AdminKit\Core\Containers\ArticleSection\Article\UI\API\Controllers; -use AdminKit\Core\Containers\ArticleSection\Article\Models\Article; +use AdminKit\Core\Containers\ArticleSection\Article\UI\API\Repositories\ArticleInterface; class ArticleController { + public function __construct( + public ArticleInterface $repository, + ) { + } + public function index() { - return Article::all(); + return $this->repository->getPaginatedList(); + } + + public function show(int $id) + { + return $this->repository->getById($id); } - public function show(Article $article) + public function showBySlug(string $slug) { - return $article; + return $this->repository->getBySlug($slug); } } diff --git a/src/Containers/ArticleSection/Article/UI/API/DTO/ArticleDTO.php b/src/Containers/ArticleSection/Article/UI/API/DTO/ArticleDTO.php new file mode 100644 index 0000000..53ea889 --- /dev/null +++ b/src/Containers/ArticleSection/Article/UI/API/DTO/ArticleDTO.php @@ -0,0 +1,39 @@ + isset($article->id), fn () => $article->id), + Lazy::when(fn () => isset($article->slug), fn () => $article->slug), + Lazy::when(fn () => isset($article->title), fn () => $article->title), + Lazy::when(fn () => isset($article->content), fn () => $article->content), + Lazy::when(fn () => isset($article->short_content), fn () => $article->short_content), + Lazy::when(fn () => isset($article->published_at), fn () => $article->published_at), + Lazy::when(fn () => isset($article->created_at), fn () => $article->created_at), + Lazy::when(fn () => isset($article->updated_at), fn () => $article->updated_at), + ); + } +} diff --git a/src/Containers/ArticleSection/Article/UI/API/Repositories/ArticleInterface.php b/src/Containers/ArticleSection/Article/UI/API/Repositories/ArticleInterface.php new file mode 100644 index 0000000..c7ab368 --- /dev/null +++ b/src/Containers/ArticleSection/Article/UI/API/Repositories/ArticleInterface.php @@ -0,0 +1,18 @@ +query('per_page'); + + $articles = QueryBuilder::for($this->model()) + ->withTranslation() + ->allowedFilters([ + 'id', + 'slug', + 'published_at', + 'created_at', + 'updated_at', + AllowedFilter::scope('title'), + AllowedFilter::scope('content'), + AllowedFilter::scope('short_content'), + 'translation.title', + 'translation.content', + 'translation.short_content', + ]) + ->allowedFields([ + 'id', + 'slug', + 'published_at', + 'created_at', + 'updated_at', + 'translation.title', + //'translation.content', + 'translation.short_content', + ]) + ->allowedSorts(['id', 'published_at']) + ->isPublished() + ->isTitleNotNull() + ->paginate($perPage); + + return ArticleDTO::collection($articles)->except('content'); + } + + /** + * @throws Exception + */ + public function getById(int $id): Data + { + $article = $this->model->findOrFail($id); + + $this->checkIsPublished($article); + + return ArticleDTO::from($article); + } + + /** + * @throws Exception + */ + public function getBySlug(string $slug): Data + { + $article = $this->model->where('slug', $slug)->first(); + + if (is_null($article)) { + throw (new ModelNotFoundException)->setModel($this->model(), $slug); + } + + $this->checkIsPublished($article); + + return ArticleDTO::from($article); + } + + private function checkIsPublished(Article $article) + { + if (is_null($article->published_at)) { + throw new Exception(__('Article has not been published')); + } + } +} diff --git a/src/Containers/ArticleSection/Article/UI/API/Routes/GetArticle.v1.public.php b/src/Containers/ArticleSection/Article/UI/API/Routes/GetArticle.v1.public.php deleted file mode 100644 index 0094e33..0000000 --- a/src/Containers/ArticleSection/Article/UI/API/Routes/GetArticle.v1.public.php +++ /dev/null @@ -1,6 +0,0 @@ -where('id', '[0-9]+'); + Route::get('/articles/slug/{slug}', [ArticleController::class, 'showBySlug']); +} diff --git a/src/CoreServiceProvider.php b/src/CoreServiceProvider.php index f60bf4d..dcf3acb 100644 --- a/src/CoreServiceProvider.php +++ b/src/CoreServiceProvider.php @@ -25,6 +25,7 @@ public function register() ->registerConfigs() ->registerLocalizations() + // use porto register ->initPorto(AdminKit::srcPath()) ->runLoaderRegister(); @@ -40,6 +41,7 @@ public function boot() ->publishConfigs() ->publishMigrations() + // use porto boot ->initPorto(AdminKit::srcPath()) ->runLoaderBoot(); } diff --git a/src/Repositories/AbstractRepository.php b/src/Repositories/AbstractRepository.php new file mode 100644 index 0000000..d02ee56 --- /dev/null +++ b/src/Repositories/AbstractRepository.php @@ -0,0 +1,53 @@ +makeModel(); + $this->boot(); + } + + public function boot() + { + // + } + + public function getModel(): Model + { + return $this->model; + } + + abstract public function model(); + + /** + * @throws BindingResolutionException + * @throws Exception + */ + public function makeModel(): Model + { + $model = app()->make($this->model()); + + if (! $model instanceof Model) { + throw new Exception("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"); + } + + return $this->model = $model; + } + + public function all($columns = ['*']): Collection + { + return $this->model->all($columns); + } +} diff --git a/src/Repositories/RepositoryInterface.php b/src/Repositories/RepositoryInterface.php new file mode 100644 index 0000000..6d32ef9 --- /dev/null +++ b/src/Repositories/RepositoryInterface.php @@ -0,0 +1,12 @@ +