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.

More info about error code prefixing, see

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
If you have some pivot columns, see

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!