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 posts entity is from blog namespace and categories entity is from core namespace.

Entity

First of all, we should create entity files by entity command.

1php artisan valravn:entity blog posts

It will create all files we needed. so let's configure the generated files.

Database

To set up related table, edit migration file in database/migration/Blog/{date}_create_posts_table.php.

 1use App\Models\Blog\Post;
 2use Illuminate\Database\Migrations\Migration;
 3use Illuminate\Database\Schema\Blueprint;
 4use Illuminate\Support\Facades\Schema;
 5
 6return new class extends Migration {
 7
 8    public function up() {
 9        Schema::create( Post::table(), function( Blueprint $table ) {
10            $table->id();
11            $table->string( 'title' );
12            $table->text( 'content' );
13            $table->timestamps();
14        } );
15    }
16
17    public function down() {
18        Schema::dropIfExists( Post::table() );
19    }
20};

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.

1valravn: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 are a 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 as your namespaces. so i create a php file in routes/app directory named blog.php. then, we should register our new file in RouteServiceProvider.

 1// app/Providers/RouteServiceProvider.php
 2
 3class RouteServiceProvider extends ServiceProvider {
 4
 5    // ...
 6
 7    public function boot() {
 8        // ...
 9        $this->routes( function() {
10            // ...
11            
12            Route::prefix( 'api/blog' )
13                 ->name( 'blog.' )
14                 ->middleware( 'api' )
15                 ->group( base_path( 'routes/app/blog.php' ) );
16                 
17        } );
18    }
19
20    // ...
21    
22}

Now, we can register our entity routes.

 1// routes/app/blog.php
 2
 3use Hans\Valravn\Facades\Router;
 4
 5Router::resource( '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 IUserRepository 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    public function boot() {
15        //
16    }
17    
18}

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\ValravnException;
 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\ValravnException;
 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\ValravnException;
 4use Symfony\Component\HttpFoundation\Response;
 5
 6class PostException extends ValravnException {
 7
 8    public static function failedToUpdateCategories(): ValravnException {
 9        return self::make(
10            "Failed to update post's categories!",
11            PostErrorCode::failedToUpdateCategories(),
12            Response::HTTP_INTERNAL_SERVER_ERROR
13        );
14    }
15
16    public static function failedToAttachCategories(): ValravnException {
17        return self::make(
18            "Failed to attach post's categories!",
19            PostErrorCode::failedToAttachCategories(),
20            Response::HTTP_INTERNAL_SERVER_ERROR
21        );
22    }
23
24    public static function failedToDetachCategories(): ValravnException {
25        return self::make(
26            "Failed to detach post's categories!",
27            PostErrorCode::failedToDetachCategories(),
28            Response::HTTP_INTERNAL_SERVER_ERROR
29        );
30    }
31
32}

Then in the PostErrorCode class, we should add new error codes.

 1// app/Exceptions/Blog/Post/PostErrorCode.php
 2
 3use Hans\Valravn\Exceptions\ErrorCode;
 4
 5class PostErrorCode extends ErrorCode {
 6    protected static string $prefix = 'PECx';
 7
 8    protected int $FAILED_TO_UPDATE_CATEGORIES = 1; // or failedToUpdateCategories
 9    protected int $FAILED_TO_ATTACH_CATEGORIES = 2;
10    protected int $FAILED_TO_DETACH_CATEGORIES = 3;
11}

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\ValravnFormRequest;
 4
 5class PostUpdateRequest extends ValravnFormRequest {
 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\ValravnFormRequest;
 4
 5class PostUpdateRequest extends ValravnFormRequest {
 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 this

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 AuthServiceProvider.

 1// app/Providers/AuthServiceProvider.php
 2
 3use App\Models\Blog\Post;
 4use App\Policies\Blog\PostPolicy;
 5use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
 6
 7class AuthServiceProvider extends ServiceProvider {
 8    /**
 9     * The policy mappings for the application.
10     *
11     * @var array<class-string, class-string>
12     */
13    protected $policies = [
14        // ...
15        Post::class => PostPolicy::class,
16    ];
17
18    /**
19     * Register any authentication / authorization services.
20     *
21     * @return void
22     */
23    public function boot() {
24        $this->registerPolicies();
25    }
26
27}