Use case
In this section, we will create an api using Valravn
to show how you should use this. let's assume there is a posts
entity and have a BelongsToMany
relationship with categories
entity. the namespace of posts
entity is blog
and categories
entity is under core
namespace.
Entity
First of all, we should create entity files by entity
command.
1php artisan valravn:entity blog posts BPEcx
The first parameter determines the namespace, The second one is the entity name and the third one is a prefix for error codes of the entity. It will create all files we needed. so let's configure the generated files.
Database
To setup the related table, edit migration file.
1// database/migration/Blog/{date}_create_posts_table.php
2
3use App\Models\Blog\Post;
4use Illuminate\Database\Migrations\Migration;
5use Illuminate\Database\Schema\Blueprint;
6use Illuminate\Support\Facades\Schema;
7
8return new class extends Migration {
9
10 public function up() {
11 Schema::create( Post::table(), function( Blueprint $table ) {
12 $table->id();
13 $table->string( 'title' );
14 $table->text( 'content' );
15 $table->timestamps();
16 } );
17 }
18
19 public function down() {
20 Schema::dropIfExists( Post::table() );
21 }
22};
As we said, there is a BelongsToMany
relationship with categories
entity, so we should have a pivot table. to create
a pivot migration file, run this command.
1php artisan valravn:pivot blog posts core categories
This will create a pivot table in migrations/Blog/
path. if you want some pivot columns, you can define your columns
here.
1// database/migrations/Blog/{data}_create_category_post_table.php
2
3use App\Models\Blog\Post;
4use App\Models\Core\Category;
5use Illuminate\Database\Migrations\Migration;
6use Illuminate\Database\Schema\Blueprint;
7use Illuminate\Support\Facades\Schema;
8
9return new class extends Migration {
10
11 public function up() {
12 Schema::create( 'category_post', function( Blueprint $table ) {
13 $table->foreignIdFor( Post::class )->constrained()->cascadeOnDelete();
14 $table->foreignIdFor( Category::class )->constrained()->cascadeOnDelete();
15
16 $table->primary( [ Post::foreignKey(), Category::foreignKey() ] );
17 } );
18 }
19
20 public function down() {
21 Schema::dropIfExists( 'category_post' );
22 }
23};
There will be factory and seeder classes that you should configure that classes too.
Routing
It's recommended to define your routes in separate files and name the files equals to your namespaces. So, I create a php
file
in routes/app
directory named blog.php
. then, the Valravn will register the route file automatically.
However, You can register the route files by your own. To do so, Set the REGISTER_ROUTES
env to false and then
register the route file like below:
1// bootstrap/app.php
2
3->withRouting(
4 // ...
5 then: function (Application $app){
6 Route::prefix('api/blog')
7 ->name( 'blog.' )
8 ->middleware( 'api' )
9 ->group(base_path('routes/app/blog.php'));
10 }
11)
Now, we can register our entity routes.
1// routes/app/blog.php
2
3use Hans\Valravn\Facades\VRouter;
4
5VRouter::apiResource( 'posts', PostCrudController::class )
6 ->withBatchUpdate()
7 ->relations(
8 PostRelationsController::class,
9 function( RelationsRegisterer $relations ) {
10 $relations->belongsToMany( 'categories' );
11 }
12 );
Repository
Next, to handle categories
relationship, add these abstract methods to IPostRepository
.
1// app/Repositories/Contract/Blog/IPostRepository.php
2
3use App\Models\Blog\Post;
4use App\Repositories\Contracts\Repository;
5use Hans\Valravn\DTOs\ManyToManyDto;
6use Illuminate\Contracts\Database\Eloquent\Builder;
7
8abstract class IPostRepository extends Repository {
9
10 abstract public function viewCategories( Post $post ): Builder;
11
12 abstract public function updateCategories( Post $post, ManyToManyDto $dto ): array;
13
14 abstract public function attachCategories( Post $post, ManyToManyDto $dto ): array;
15
16 abstract public function detachCategories( Post $post, ManyToManyDto $dto ): int;
17
18}
Then, implement these methods on PostRepository
.
1// app/Repositories/Blog/PostRepository.php
2
3use App\Models\Core\Category;
4use App\Models\Blog\Post;
5use App\Repositories\Contracts\Blog\IPostRepository;
6use Hans\Valravn\DTOs\ManyToManyDto;
7use Illuminate\Auth\Access\AuthorizationException;
8use Illuminate\Contracts\Database\Eloquent\Builder;
9
10class PostRepository extends IPostRepository {
11
12 protected function getQueryBuilder(): Builder {
13 return Post::query();
14 }
15
16 public function viewCategories( Category $model ): Builder {
17 $this->authorize( $model );
18
19 return $model->categories();
20 }
21
22 public function updateCategories( Category $model, ManyToManyDto $dto ): array {
23 $this->authorizeThisAction( $model, $dto->getData() );
24
25 return $model->categories()->sync( $dto->getData() );
26 }
27
28 public function attachCategories( Category $model, ManyToManyDto $dto ): array {
29 $this->authorizeThisAction( $model, $dto->getData() );
30
31 return $model->categories()->syncWithoutDetaching( $dto->getData() );
32 }
33
34 public function detachCategories( Category $model, ManyToManyDto $dto ): int {
35 $this->authorizeThisAction( $model, $dto->getData() );
36
37 return $model->categories()->detach( $dto->getData() );
38 }
39}
In the end, we should bind IPostRepository
contract to PostRepository
class in the RepositoryServiceProvider
.
1// app/Providers/RepositoryServiceProvider.php
2
3use App\Repositories\Contracts\Blog\IPostRepository;
4use App\Repositories\Blog\PostRepository;
5use Illuminate\Support\ServiceProvider;
6
7class RepositoryServiceProvider extends ServiceProvider {
8
9 public function register() {
10 // ...
11 $this->app->bind( IPostRepository::class, PostRepository::class );
12 }
13
14 //...
15}
So, we are done with repositories.
Services
Crud service
In service layer, we should create CRUD actions in PostCrudService
first.
1// app/Services/Blog/Post/PostCrudService.php
2
3use App\Exceptions\Blog\Post\PostException;
4use App\Models\Blog\Post;
5use App\Repositories\Contracts\Blog\IPostRepository;
6use Hans\Valravn\DTOs\BatchUpdateDto;
7use Hans\Valravn\Exceptions\VException;
8use Illuminate\Auth\Access\AuthorizationException;
9use Illuminate\Contracts\Pagination\Paginator;
10use Throwable;
11
12class PostCrudService {
13 private IPostRepository $repository;
14
15 public function __construct() {
16 $this->repository = app( IPostRepository::class );
17 }
18
19 public function all(): Paginator {
20 return $this->repository->all()->applyFilters()->paginate();
21 }
22
23 public function create( array $data ): Post {
24 throw_unless(
25 $model = $this->repository->create( $data ),
26 PostException::failedToCreate()
27 );
28
29 return $model;
30 }
31
32 public function find( int $model ): Post {
33 return $this->repository->find( $model );
34 }
35
36 public function update( Post $model, array $data ): Post {
37 throw_unless(
38 $this->repository->update( $model, $data ),
39 PostException::failedToUpdate()
40 );
41
42 return $model;
43 }
44
45 public function batchUpdate( BatchUpdateDto $dto ): Paginator {
46 if ( $this->repository->batchUpdate( $dto ) ) {
47 return $this->repository->all()
48 ->whereIn( 'id', $dto->getData()->pluck( 'id' ) )
49 ->applyFilters()
50 ->paginate();
51 }
52
53 throw PostException::failedToBatchUpdate();
54 }
55
56 public function delete( Post $model ): Post {
57 throw_unless( $this->repository->delete( $model ), PostException::failedToDelete() );
58
59 return $model;
60 }
61}
Relations service
After Crud service, we should set up PostRelationsService
.
1// app/Services/Blog/Post/PostRelationsService.php
2
3use App\Exceptions\Blog\Post\PostException;
4use App\Models\Blog\Post;
5use App\Repositories\Contracts\Blog\IPostRepository;
6use Hans\Valravn\DTOs\ManyToManyDto;
7use Hans\Valravn\Exceptions\VException;
8use Illuminate\Contracts\Pagination\Paginator;
9
10class PostRelationsService {
11 private IPostRepository $repository;
12
13 public function __construct() {
14 $this->repository = app( IPostRepository::class );
15 }
16
17 public function viewCategories( Post $model ): Paginator {
18 return $this->repository->viewCategories( $model )->applyFilters()->paginate();
19 }
20
21 public function updateCategories( Post $model, ManyToManyDto $dto ): Paginator {
22 if ( $this->repository->updateCategories( $model, $dto ) ) {
23 return $this->viewCategories( $model );
24 }
25
26 throw PostException::failedToUpdateCategories();
27 }
28
29 public function attachCategories( Post $model, ManyToManyDto $dto ): Paginator {
30 if ( $this->repository->attachCategories( $model, $dto ) ) {
31 return $this->viewCategories( $model );
32 }
33
34 throw PostException::failedToAttachCategories();
35 }
36
37 public function detachCategories( Post $model, ManyToManyDto $dto ): Paginator {
38 if ( $this->repository->detachCategories( $model, $dto ) ) {
39 return $this->viewCategories( $model );
40 }
41
42 throw PostException::failedToDetachCategories();
43 }
44
45}
Exceptions
To handle PostRelationsService
needed exceptions, open PostException
class and add these methods.
1// app/Exceptions/Blog/Post/PostException.php
2
3use Hans\Valravn\Exceptions\VException;
4use Symfony\Component\HttpFoundation\Response;
5
6class PostException extends VException {
7
8 protected string $errorCodePrefix = 'BPEcx';
9
10 public static function failedToUpdateCategories(): VException {
11 return new self("Failed to update post's categories!", 1);
12 }
13
14 public static function failedToAttachCategories(): VException {
15 return new self("Failed to attach post's categories!", 2);
16 }
17
18 public static function failedToDetachCategories(): VException {
19 return new self("Failed to detach post's categories!", 3);
20 }
21
22}
Controllers
Crud controller
The PostCrudController
created in app/Http/Controllers/V1/Blog/Post/PostCrudController.php
. The created crud
controller has the ability to handling the crud actions. However, if you want to customize the methods, you are free to
make your changes.
Crud requests
In addition, we should set up our PostStoreRequest
and PostUpdateRequest
requests.
1// app/Http/Requests/V1/Blog/Post/PostStoreRequest.php
2
3use Hans\Valravn\Http\Requests\Contracts\VFormRequest;
4
5class PostUpdateRequest extends VFormRequest {
6
7 protected function fields(): array {
8 return [
9 'title' => [ 'required', 'string', 'max:255' ],
10 'content' => [ 'required', 'string' ],
11 ];
12 }
13}
Also, we have these rules for updating request.
1// app/Http/Requests/V1/Blog/Post/PostUpdateRequest.php
2
3use Hans\Valravn\Http\Requests\Contracts\VFormRequest;
4
5class PostUpdateRequest extends VFormRequest {
6
7 protected function fields(): array {
8 return [
9 'title' => [ 'string', 'max:255' ],
10 'content' => [ 'string' ],
11 ];
12 }
13}
Relations controller
As we have a BelongsToMany
relationship, we should create related relation request using below command.
1php artisan valravn:relation blog posts core categories --belongs-to-many
Next, we should add the needed methods to our relations controller.
1// app/Http/Controllers/V1/Blog/Post/PostRelationsController.php
2
3use App\Http\Controllers\Controller;
4use App\Http\Requests\V1\Blog\Post\PostCategoriesRequest;
5use App\Http\Resources\V1\Core\Category\CategoryCollection;
6use App\Models\Blog\Post;
7use App\Models\Core\Category;
8use App\Services\Blog\Post\PostRelationsService;
9use Hans\Valravn\DTOs\ManyToManyDto;
10
11class PostRelationsController extends Controller {
12
13 private PostRelationsService $service;
14
15 public function __construct() {
16 $this->service = app( PostRelationsService::class );
17 }
18
19 public function viewCategories( Post $post ): CategoryCollection {
20 return Category::getResourceCollection( $this->service->viewCategories( $post ) );
21 }
22
23 public function updateCategories( Post $post, PostCategoriesRequest $request ): CategoryCollection {
24 return Category::getResourceCollection(
25 $this->service->updateCategories( $post, ManyToManyDto::make( $request->validated() ) )
26 );
27 }
28
29 public function attachCategories( Post $post, PostCategoriesRequest $request ): CategoryCollection {
30 return Category::getResourceCollection(
31 $this->service->attachCategories( $post, ManyToManyDto::make( $request->validated() ) )
32 );
33 }
34
35 public function detachOwned( Post $post, PostCategoriesRequest $request ): CategoryCollection {
36 return Category::getResourceCollection(
37 $this->service->detachOwned( $post, ManyToManyDto::make( $request->validated() ) )
38 );
39 }
40
41}
Policy
In continue, to register PostPolicy
class to related model, should register it in the AppServiceProvider
.
1// app/Providers/AppServiceProvider.php
2
3use App\Models\Blog\Post;
4use App\Policies\Blog\PostPolicy;
5use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
6
7class AppServiceProvider extends ServiceProvider
8{
9 /**
10 * Register any application services.
11 */
12 public function register(): void
13 {
14 //...
15 Gate::policy(Post::class, PostPolicy::class);
16 }
17
18 //...
19}
Finally, our api service for working with posts
entity and its relationship with categories
is ready!