diff --git a/app/Http/Api/Controllers/AuthorController.php b/app/Http/Api/Controllers/AuthorController.php new file mode 100644 index 0000000..605729d --- /dev/null +++ b/app/Http/Api/Controllers/AuthorController.php @@ -0,0 +1,102 @@ +validated(); + + $parameters = new FindAuthorParameters( + name: $data['name'] ?? null, + sort: $data['sort'] ?? null, + page: $data['page'] ?? null, + perPage: $data['perPage'] ?? null, + withSongsCount: true + ); + + $repository = app(AuthorRepository::class); + + $authors = $repository->find($parameters); + + return AuthorResource::collection($authors); + } + + public function show(Author $author): AuthorResource + { + $author->loadCount('songs'); + + return new AuthorResource($author); + } + + public function store(StoreAuthorRequest $request): AuthorResource + { + $data = $request->validated(); + + $parameters = new StoreAuthorParameters( + name: $data['name'], + foundationYear: $data['foundation_year'], + website: $data['website'] ?? null + ); + + $repository = app(AuthorRepository::class); + + try { + $author = $repository->store($parameters); + } catch (Throwable $exception) { + abort(HttpFoundationResponse::HTTP_INTERNAL_SERVER_ERROR, $exception->getMessage()); + } + + return new AuthorResource($author); + } + + public function update(UpdateAuthorRequest $request, Author $author): AuthorResource + { + $data = $request->validated(); + + $parameters = new UpdateAuthorParameters( + name: $data['name'] ?? null, + foundationYear: $data['foundation_year'] ?? null, + website: $data['website'] ?? null + ); + + $repository = app(AuthorRepository::class); + + try { + $author = $repository->update($author, $parameters); + } catch (Throwable $exception) { + abort(HttpFoundationResponse::HTTP_INTERNAL_SERVER_ERROR, $exception->getMessage()); + } + + return new AuthorResource($author); + } + + public function delete(Author $author): JsonResponse + { + $repository = app(AuthorRepository::class); + + try { + $repository->delete($author); + } catch (Throwable $exception) { + abort(HttpFoundationResponse::HTTP_INTERNAL_SERVER_ERROR, $exception->getMessage()); + } + + return response()->json(['message' => __('messages.deleted_successfully')]); + } +} diff --git a/app/Http/Api/Controllers/SongController.php b/app/Http/Api/Controllers/SongController.php new file mode 100644 index 0000000..795c90c --- /dev/null +++ b/app/Http/Api/Controllers/SongController.php @@ -0,0 +1,111 @@ +validated(); + + $parameters = new FindSongParameters( + title: $data['title'] ?? null, + authorId: $data['author_id'] ?? null, + albumId: $data['album_id'] ?? null, + sort: $data['sort'] ?? null, + page: $data['page'] ?? 1, + perPage: $data['per_page'] ?? 10, + withGenre: true + ); + + $repository = app(SongRepository::class); + + $songs = $repository->find($parameters); + + return SongResource::collection($songs); + } + + public function show(Song $song): SongResource + { + $song->load('genre'); + + return new SongResource($song); + } + + public function store(StoreSongRequest $request): SongResource + { + $data = $request->validated(); + + $parameters = new StoreSongParameters( + title: $data['title'], + durationInSeconds: $data['duration_in_seconds'], + authorId: $data['author_id'], + albumId: $data['album_id'], + genreId: $data['genre_id'] + ); + + $repository = app(SongRepository::class); + + try { + $song = $repository->store($parameters); + } catch (Throwable $exception) { + abort(HttpFoundationResponse::HTTP_INTERNAL_SERVER_ERROR, $exception->getMessage()); + } + + return new SongResource($song); + } + + public function update(UpdateSongRequest $request, Song $song): SongResource + { + $data = $request->validated(); + + $parameters = new UpdateSongParameters( + title: $data['title'] ?? null, + durationInSeconds: $data['duration_in_seconds'] ?? null, + authorId: $data['author_id'] ?? null, + albumId: $data['album_id'] ?? null, + genreId: $data['genre_id'] ?? null + ); + + $repository = app(SongRepository::class); + + try { + $song = $repository->update( + song: $song, + parameters: $parameters + ); + } catch (Throwable $exception) { + abort(HttpFoundationResponse::HTTP_INTERNAL_SERVER_ERROR, $exception->getMessage()); + } + + return new SongResource($song); + } + + public function delete(Song $song): JsonResponse + { + $repository = app(SongRepository::class); + + try { + $repository->delete($song); + } catch (Throwable $exception) { + abort(HttpFoundationResponse::HTTP_INTERNAL_SERVER_ERROR, $exception->getMessage()); + } + + return response()->json(['message' => __('messages.deleted_successfully')]); + } +} diff --git a/app/Http/Api/Requests/AuthorListRequest.php b/app/Http/Api/Requests/AuthorListRequest.php new file mode 100644 index 0000000..075c031 --- /dev/null +++ b/app/Http/Api/Requests/AuthorListRequest.php @@ -0,0 +1,34 @@ + [ + 'sometimes', + 'string', + ], + 'sort' => [ + 'sometimes', + 'string', + 'in:songs_count,foundation_year', + ], + 'page' => [ + 'sometimes', + 'integer', + 'min:1', + ], + 'per_page' => [ + 'sometimes', + 'integer', + 'min:10', + 'max:25', + ], + ]; + } +} diff --git a/app/Http/Api/Requests/SongListRequest.php b/app/Http/Api/Requests/SongListRequest.php new file mode 100644 index 0000000..3959b07 --- /dev/null +++ b/app/Http/Api/Requests/SongListRequest.php @@ -0,0 +1,46 @@ + [ + 'sometimes', + 'string', + ], + 'author_id' => [ + 'sometimes', + 'integer', + 'exists:' . Author::class . ',id', + ], + 'album_id' => [ + 'sometimes', + 'integer', + 'exists:' . Album::class . ',id', + ], + 'sort' => [ + 'sometimes', + 'string', + 'in:title,duration_in_seconds,genre_id', + ], + 'page' => [ + 'sometimes', + 'integer', + 'min:1', + ], + 'per_page' => [ + 'sometimes', + 'integer', + 'min:10', + 'max:25', + ], + ]; + } +} diff --git a/app/Http/Api/Requests/StoreAuthorRequest.php b/app/Http/Api/Requests/StoreAuthorRequest.php new file mode 100644 index 0000000..b229c2f --- /dev/null +++ b/app/Http/Api/Requests/StoreAuthorRequest.php @@ -0,0 +1,29 @@ + [ + 'required', + 'string', + ], + 'foundation_year' => [ + 'required', + 'integer', + 'min:1700', + 'max:' . Carbon::now()->year, + ], + 'website' => [ + 'sometimes', + 'url' + ], + ]; + } +} diff --git a/app/Http/Api/Requests/StoreSongRequest.php b/app/Http/Api/Requests/StoreSongRequest.php new file mode 100644 index 0000000..5dbb48b --- /dev/null +++ b/app/Http/Api/Requests/StoreSongRequest.php @@ -0,0 +1,41 @@ + [ + 'required', + 'string', + ], + 'duration_in_seconds' => [ + 'required', + 'integer', + 'min:1' + ], + 'author_id' => [ + 'required', + 'integer', + 'exists:' . Author::class . ',id', + ], + 'album_id' => [ + 'required', + 'integer', + 'exists:' . Album::class . ',id', + ], + 'genre_id' => [ + 'required', + 'integer', + 'exists:' . Genre::class . ',id', + ], + ]; + } +} diff --git a/app/Http/Api/Requests/UpdateAuthorRequest.php b/app/Http/Api/Requests/UpdateAuthorRequest.php new file mode 100644 index 0000000..8cae26d --- /dev/null +++ b/app/Http/Api/Requests/UpdateAuthorRequest.php @@ -0,0 +1,29 @@ + [ + 'sometimes', + 'string', + ], + 'foundation_year' => [ + 'sometimes', + 'integer', + 'min:1700', + 'max:' . Carbon::now()->year, + ], + 'website' => [ + 'sometimes', + 'url' + ], + ]; + } +} diff --git a/app/Http/Api/Requests/UpdateSongRequest.php b/app/Http/Api/Requests/UpdateSongRequest.php new file mode 100644 index 0000000..16ae23d --- /dev/null +++ b/app/Http/Api/Requests/UpdateSongRequest.php @@ -0,0 +1,41 @@ + [ + 'sometimes', + 'string', + ], + 'duration_in_seconds' => [ + 'sometimes', + 'integer', + 'min:1' + ], + 'author_id' => [ + 'sometimes', + 'integer', + 'exists:' . Author::class . ',id', + ], + 'album_id' => [ + 'sometimes', + 'integer', + 'exists:' . Album::class . ',id', + ], + 'genre_id' => [ + 'sometimes', + 'integer', + 'exists:' . Genre::class . ',id', + ], + ]; + } +} diff --git a/app/Http/Api/Resources/AuthorResource.php b/app/Http/Api/Resources/AuthorResource.php new file mode 100644 index 0000000..c0281e7 --- /dev/null +++ b/app/Http/Api/Resources/AuthorResource.php @@ -0,0 +1,29 @@ + $this->resource->id, + 'name' => $this->resource->name, + 'foundation_year' => $this->resource->foundation_year, + 'songs_count' => $this->resource->songs_count ?? 0, + 'website' => $this->resource->website, + ]; + } +} diff --git a/app/Http/Api/Resources/SongResource.php b/app/Http/Api/Resources/SongResource.php new file mode 100644 index 0000000..1d10c64 --- /dev/null +++ b/app/Http/Api/Resources/SongResource.php @@ -0,0 +1,31 @@ + $this->resource->id, + 'title' => $this->resource->title, + 'duration_in_seconds' => $this->resource->duration_in_seconds, + 'genre_id' => $this->resource->genre_id, + 'genre' => $this->resource->genre->title, + 'author_id' => $this->resource->author_id, + 'album_id' => $this->resource->album_id, + ]; + } +} diff --git a/app/Models/Album.php b/app/Models/Album.php new file mode 100644 index 0000000..022d3ea --- /dev/null +++ b/app/Models/Album.php @@ -0,0 +1,43 @@ + $songs + * @property-read int|null $songs_count + * @method static Builder|Album newModelQuery() + * @method static Builder|Album newQuery() + * @method static Builder|Album query() + * @method static Builder|Album whereAuthorId($value) + * @method static Builder|Album whereCreatedAt($value) + * @method static Builder|Album whereId($value) + * @method static Builder|Album whereTitle($value) + * @method static Builder|Album whereUpdatedAt($value) + * @mixin Eloquent + */ +class Album extends Model +{ + public function author(): BelongsTo + { + return $this->belongsTo(Author::class); + } + + public function songs(): HasMany + { + return $this->hasMany(Song::class); + } +} diff --git a/app/Models/Author.php b/app/Models/Author.php new file mode 100644 index 0000000..a66c9ce --- /dev/null +++ b/app/Models/Author.php @@ -0,0 +1,45 @@ + $albums + * @property-read int|null $albums_count + * @property-read Collection $songs + * @property-read int|null $songs_count + * @method static Builder|Author newModelQuery() + * @method static Builder|Author newQuery() + * @method static Builder|Author query() + * @method static Builder|Author whereCreatedAt($value) + * @method static Builder|Author whereFoundationYear($value) + * @method static Builder|Author whereId($value) + * @method static Builder|Author whereName($value) + * @method static Builder|Author whereUpdatedAt($value) + * @method static Builder|Author whereWebsite($value) + * @mixin Eloquent + */ +class Author extends Model +{ + public function albums(): HasMany + { + return $this->hasMany(Album::class); + } + + public function songs(): HasMany + { + return $this->hasMany(Song::class); + } +} diff --git a/app/Models/Genre.php b/app/Models/Genre.php new file mode 100644 index 0000000..174ac48 --- /dev/null +++ b/app/Models/Genre.php @@ -0,0 +1,34 @@ + $songs + * @property-read int|null $songs_count + * @method static Builder|Genre newModelQuery() + * @method static Builder|Genre newQuery() + * @method static Builder|Genre query() + * @method static Builder|Genre whereCreatedAt($value) + * @method static Builder|Genre whereId($value) + * @method static Builder|Genre whereTitle($value) + * @method static Builder|Genre whereUpdatedAt($value) + * @mixin Eloquent + */ +class Genre extends Model +{ + public function songs(): HasMany + { + return $this->hasMany(Song::class); + } +} diff --git a/app/Models/Song.php b/app/Models/Song.php new file mode 100644 index 0000000..0884fb0 --- /dev/null +++ b/app/Models/Song.php @@ -0,0 +1,52 @@ +|Song newModelQuery() + * @method static Builder|Song newQuery() + * @method static Builder|Song query() + * @method static Builder|Song whereAlbumId($value) + * @method static Builder|Song whereAuthorId($value) + * @method static Builder|Song whereCreatedAt($value) + * @method static Builder|Song whereDurationInSeconds($value) + * @method static Builder|Song whereGenreId($value) + * @method static Builder|Song whereId($value) + * @method static Builder|Song whereTitle($value) + * @method static Builder|Song whereUpdatedAt($value) + * @mixin Eloquent + */ +class Song extends Model +{ + public function genre(): BelongsTo + { + return $this->belongsTo(Genre::class); + } + + public function album(): BelongsTo + { + return $this->belongsTo(Album::class); + } + + public function author(): BelongsTo + { + return $this->belongsTo(Author::class); + } +} diff --git a/app/Repositories/AuthorRepository.php b/app/Repositories/AuthorRepository.php new file mode 100644 index 0000000..9ec375c --- /dev/null +++ b/app/Repositories/AuthorRepository.php @@ -0,0 +1,72 @@ +when($parameters->withSongsCount, fn(Builder $query) => $query->withCount('songs')) + ->when($parameters->name, fn(Builder $query) => $query->where('name', 'like', "%$parameters->name%")) + ->when($parameters->sort, fn (Builder $query) => $query->orderBy($parameters->sort)) + ->paginate(perPage: $parameters->perPage, page: $parameters->page); + } + + /** + * @throws Throwable + */ + public function store(StoreAuthorParameters $parameters): Author + { + $author = new Author(); + + $author->name = $parameters->name; + $author->foundation_year = $parameters->foundationYear; + + if (!is_null($parameters->website)) { + $author->website = $parameters->website; + } + + $author->saveOrFail(); + + return $author; + } + + /** + * @throws Throwable + */ + public function update(Author $author, UpdateAuthorParameters $parameters): Author + { + if (!is_null($parameters->name)) { + $author->name = $parameters->name; + } + + if (!is_null($parameters->foundationYear)) { + $author->foundation_year = $parameters->foundationYear; + } + + if (!is_null($parameters->website)) { + $author->website = $parameters->website; + } + + $author->saveOrFail(); + + return $author; + } + + /** + * @throws Throwable + */ + public function delete(Author $author): void + { + $author->deleteOrFail(); + } +} diff --git a/app/Repositories/Parameters/FindAuthorParameters.php b/app/Repositories/Parameters/FindAuthorParameters.php new file mode 100644 index 0000000..be56c82 --- /dev/null +++ b/app/Repositories/Parameters/FindAuthorParameters.php @@ -0,0 +1,15 @@ +when($parameters->withGenre, fn(Builder $query) => $query->with('genre')) + ->when($parameters->withAlbum, fn(Builder $query) => $query->with('album')) + ->when($parameters->withAuthor, fn(Builder $query) => $query->with('author')) + ->when($parameters->title, fn(Builder $query) => $query->where('title', 'like', "%$parameters->title%")) + ->when($parameters->albumId, fn(Builder $query) => $query->where('album_id', $parameters->albumId)) + ->when($parameters->authorId, fn(Builder $query) => $query->where('author_id', $parameters->authorId)) + ->when($parameters->sort, fn (Builder $query) => $query->orderBy($parameters->sort)) + ->paginate(perPage: $parameters->perPage, page: $parameters->page); + } + + /** + * @throws Throwable + */ + public function store(StoreSongParameters $parameters): Song + { + $song = new Song(); + + $song->title = $parameters->title; + $song->duration_in_seconds = $parameters->durationInSeconds; + $song->album_id = $parameters->albumId; + $song->author_id = $parameters->authorId; + $song->genre_id = $parameters->genreId; + + $song->saveOrFail(); + + return $song; + } + + /** + * @throws Throwable + */ + public function update(Song $song, UpdateSongParameters $parameters): Song + { + if (!is_null($parameters->title)) { + $song->title = $parameters->title; + } + + if (!is_null($parameters->durationInSeconds)) { + $song->duration_in_seconds = $parameters->durationInSeconds; + } + + if (!is_null($parameters->albumId)) { + $song->album_id = $parameters->albumId; + } + + if (!is_null($parameters->authorId)) { + $song->author_id = $parameters->authorId; + } + + if (!is_null($parameters->genreId)) { + $song->genre_id = $parameters->genreId; + } + + $song->saveOrFail(); + + return $song; + } + + /** + * @throws Throwable + */ + public function delete(Song $song): void + { + $song->deleteOrFail(); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..d8fd845 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -9,6 +9,7 @@ web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', + api: __DIR__.'/../routes/api.php', ) ->withMiddleware(function (Middleware $middleware): void { // diff --git a/database/migrations/2025_10_02_085059_create_authors_table.php b/database/migrations/2025_10_02_085059_create_authors_table.php new file mode 100644 index 0000000..af3f898 --- /dev/null +++ b/database/migrations/2025_10_02_085059_create_authors_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('name')->index(); + $table->integer('foundation_year'); + $table->string('website')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('authors'); + } +}; diff --git a/database/migrations/2025_10_02_085200_create_albums_table.php b/database/migrations/2025_10_02_085200_create_albums_table.php new file mode 100644 index 0000000..b36df07 --- /dev/null +++ b/database/migrations/2025_10_02_085200_create_albums_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('title'); + + $table->foreignId('author_id') + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('albums'); + } +}; diff --git a/database/migrations/2025_10_02_085256_create_genres_table.php b/database/migrations/2025_10_02_085256_create_genres_table.php new file mode 100644 index 0000000..55ab5c2 --- /dev/null +++ b/database/migrations/2025_10_02_085256_create_genres_table.php @@ -0,0 +1,22 @@ +id(); + $table->string('title'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('genres'); + } +}; diff --git a/database/migrations/2025_10_02_085311_create_songs_table.php b/database/migrations/2025_10_02_085311_create_songs_table.php new file mode 100644 index 0000000..412c033 --- /dev/null +++ b/database/migrations/2025_10_02_085311_create_songs_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('title')->index(); + $table->integer('duration_in_seconds'); + + $table->foreignId('album_id') + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('author_id') + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('genre_id') + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('songs'); + } +}; diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 0000000..6598e2c --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,20 @@ + 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/lang/en/messages.php b/lang/en/messages.php new file mode 100644 index 0000000..baa559b --- /dev/null +++ b/lang/en/messages.php @@ -0,0 +1,5 @@ + 'Deleted successfully', +]; diff --git a/lang/en/pagination.php b/lang/en/pagination.php new file mode 100644 index 0000000..d481411 --- /dev/null +++ b/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100644 index 0000000..fad3a7d --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset.', + 'sent' => 'We have emailed your password reset link.', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 0000000..bd9cc11 --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,199 @@ + 'The :attribute field must be accepted.', + 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', + 'active_url' => 'The :attribute field must be a valid URL.', + 'after' => 'The :attribute field must be a date after :date.', + 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', + 'alpha' => 'The :attribute field must only contain letters.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', + 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'any_of' => 'The :attribute field is invalid.', + 'array' => 'The :attribute field must be an array.', + 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', + 'before' => 'The :attribute field must be a date before :date.', + 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute field must have between :min and :max items.', + 'file' => 'The :attribute field must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute field must be between :min and :max.', + 'string' => 'The :attribute field must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', + 'confirmed' => 'The :attribute field confirmation does not match.', + 'contains' => 'The :attribute field is missing a required value.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute field must be a valid date.', + 'date_equals' => 'The :attribute field must be a date equal to :date.', + 'date_format' => 'The :attribute field must match the format :format.', + 'decimal' => 'The :attribute field must have :decimal decimal places.', + 'declined' => 'The :attribute field must be declined.', + 'declined_if' => 'The :attribute field must be declined when :other is :value.', + 'different' => 'The :attribute field and :other must be different.', + 'digits' => 'The :attribute field must be :digits digits.', + 'digits_between' => 'The :attribute field must be between :min and :max digits.', + 'dimensions' => 'The :attribute field has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', + 'email' => 'The :attribute field must be a valid email address.', + 'ends_with' => 'The :attribute field must end with one of the following: :values.', + 'enum' => 'The selected :attribute is invalid.', + 'exists' => 'The selected :attribute is invalid.', + 'extensions' => 'The :attribute field must have one of the following extensions: :values.', + 'file' => 'The :attribute field must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute field must have more than :value items.', + 'file' => 'The :attribute field must be greater than :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than :value.', + 'string' => 'The :attribute field must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute field must have :value items or more.', + 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be greater than or equal to :value.', + 'string' => 'The :attribute field must be greater than or equal to :value characters.', + ], + 'hex_color' => 'The :attribute field must be a valid hexadecimal color.', + 'image' => 'The :attribute field must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field must exist in :other.', + 'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.', + 'integer' => 'The :attribute field must be an integer.', + 'ip' => 'The :attribute field must be a valid IP address.', + 'ipv4' => 'The :attribute field must be a valid IPv4 address.', + 'ipv6' => 'The :attribute field must be a valid IPv6 address.', + 'json' => 'The :attribute field must be a valid JSON string.', + 'list' => 'The :attribute field must be a list.', + 'lowercase' => 'The :attribute field must be lowercase.', + 'lt' => [ + 'array' => 'The :attribute field must have less than :value items.', + 'file' => 'The :attribute field must be less than :value kilobytes.', + 'numeric' => 'The :attribute field must be less than :value.', + 'string' => 'The :attribute field must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute field must not have more than :value items.', + 'file' => 'The :attribute field must be less than or equal to :value kilobytes.', + 'numeric' => 'The :attribute field must be less than or equal to :value.', + 'string' => 'The :attribute field must be less than or equal to :value characters.', + ], + 'mac_address' => 'The :attribute field must be a valid MAC address.', + 'max' => [ + 'array' => 'The :attribute field must not have more than :max items.', + 'file' => 'The :attribute field must not be greater than :max kilobytes.', + 'numeric' => 'The :attribute field must not be greater than :max.', + 'string' => 'The :attribute field must not be greater than :max characters.', + ], + 'max_digits' => 'The :attribute field must not have more than :max digits.', + 'mimes' => 'The :attribute field must be a file of type: :values.', + 'mimetypes' => 'The :attribute field must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute field must have at least :min items.', + 'file' => 'The :attribute field must be at least :min kilobytes.', + 'numeric' => 'The :attribute field must be at least :min.', + 'string' => 'The :attribute field must be at least :min characters.', + ], + 'min_digits' => 'The :attribute field must have at least :min digits.', + 'missing' => 'The :attribute field must be missing.', + 'missing_if' => 'The :attribute field must be missing when :other is :value.', + 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', + 'missing_with' => 'The :attribute field must be missing when :values is present.', + 'missing_with_all' => 'The :attribute field must be missing when :values are present.', + 'multiple_of' => 'The :attribute field must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute field format is invalid.', + 'numeric' => 'The :attribute field must be a number.', + 'password' => [ + 'letters' => 'The :attribute field must contain at least one letter.', + 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', + 'numbers' => 'The :attribute field must contain at least one number.', + 'symbols' => 'The :attribute field must contain at least one symbol.', + 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.', + ], + 'present' => 'The :attribute field must be present.', + 'present_if' => 'The :attribute field must be present when :other is :value.', + 'present_unless' => 'The :attribute field must be present unless :other is :value.', + 'present_with' => 'The :attribute field must be present when :values is present.', + 'present_with_all' => 'The :attribute field must be present when :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.', + 'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', + 'regex' => 'The :attribute field format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', + 'required_if_declined' => 'The :attribute field is required when :other is declined.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute field must match :other.', + 'size' => [ + 'array' => 'The :attribute field must contain :size items.', + 'file' => 'The :attribute field must be :size kilobytes.', + 'numeric' => 'The :attribute field must be :size.', + 'string' => 'The :attribute field must be :size characters.', + ], + 'starts_with' => 'The :attribute field must start with one of the following: :values.', + 'string' => 'The :attribute field must be a string.', + 'timezone' => 'The :attribute field must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'uppercase' => 'The :attribute field must be uppercase.', + 'url' => 'The :attribute field must be a valid URL.', + 'ulid' => 'The :attribute field must be a valid ULID.', + 'uuid' => 'The :attribute field must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/lang/ru/messages.php b/lang/ru/messages.php new file mode 100644 index 0000000..ba5d31d --- /dev/null +++ b/lang/ru/messages.php @@ -0,0 +1,5 @@ + 'Успешно удалено', +]; diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..a556fa2 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,20 @@ +group(function (){ + Route::get('/{author}', [AuthorController::class, 'show']); + Route::get('/', [AuthorController::class, 'index']); + Route::post('/', [AuthorController::class, 'store']); + Route::put('/{author}', [AuthorController::class, 'update']); + Route::delete('/{author}', [AuthorController::class, 'delete']); +}); + +Route::prefix('songs')->group(function (){ + Route::get('/{song}', [SongController::class, 'show']); + Route::get('/', [SongController::class, 'index']); + Route::post('/', [SongController::class, 'store']); + Route::put('/{song}', [SongController::class, 'update']); + Route::delete('/{song}', [SongController::class, 'delete']); +});