axyr / laravel-tractor
Opiniated Laravel CRUD generator for a Module/Repository/Filter based CRUD setup.
Requires
- php: ^8.2
- laravel/framework: ^11.0
- spatie/laravel-permission: ^6.0
- wikimedia/composer-merge-plugin: ^2.1
Requires (Dev)
- orchestra/testbench: ^9.1
- roave/security-advisories: dev-latest
README
Scaffold a Laravel module structure for JSON API's.
Introduction
Laravel Tractor is another module scaffolder for generating the basic PHP classes commonly used for JSON API's. The scaffolder is mainly targetted on stand alone API's without any Blade, Livewire or any other frontend library.
After installing, you can generate all required files for a working CRUD based module:
php artisan tractor:generate Post
Above command will generate a basic module structure with all the files needed for a model based JSON API.
The module will have boilerplate tests that tests all the permission authorization, database operations and filters.
📂 app
📂 app-modules
┗ 📂 Posts
┗ 📂 src
┗ 📂 Factories
┗ 📄 PostFactory.php
┗ 📂 Filters
┗ 📄 PostFilter.php
┗ 📂 Http
┗ 📂 Controllers
┗ 📄 PostController.php
┗ 📂 Requests
┗ 📄 PostRequest.php
┗ 📂 Resources
┗ 📄 PostResource.php
┗ 📂 Models
┗ 📄 Post.php
┗ 📂 Policies
┗ 📄 PostPolicy.php
┗ 📂 Repositories
┗ 📄 PostRepository.php
┗ 📂 Seeders
┗ 📄 PostSeeder.php
┗ 📂 tests
┗ 📂 Filters
┗ 📄 PostFilterTest.php
┗ 📂 Http
┗ 📂 Controllers
┗ 📄 PostControllerAuthorizationTest.php
┗ 📄 PostControllerTest.php
┗ 📄 composer.json
┗ 📄 routes.php
📂 database
┗ 📂 migrations
┗ 📄 2024_08_16_135340_create_posts_table.php
Examples
A full example of a generater module can be found here:
TODO
We generate the bare minimum, but workable and testable code, some examples:
Model
<?php namespace App\Modules\Posts\Models; use Axyr\CrudGenerator\Filters\Traits\FiltersRecords; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; /** * @property int $id * * @property Carbon $created_at * @property Carbon $updated_at */ class Post extends Model { use FiltersRecords; protected $guarded = ['id']; }
Controller
<?php namespace Modules\Posts\Http\Controllers; use Illuminate\Routing\Controller; use Modules\Posts\Models\Post; use Modules\Posts\Repositories\PostRepository; use Modules\Posts\Http\Requests\PostRequest; use Modules\Posts\Http\Resources\PostResource; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Response; class PostController extends Controller { use AuthorizesRequests; public function __construct(protected PostRepository $repository) { $this->authorizeResource(Post::class); } public function index(Request $request): ResourceCollection { $posts = $this->repository->setRequest($request)->paginate(); return PostResource::collection($posts)->preserveQuery(); } public function store(PostRequest $request): PostResource { $post = Post::query()->create($request->validated()); return new PostResource($post); } public function show(Post $post): PostResource { return new PostResource($post); } public function update(PostRequest $request, Post $post): PostResource { $post->update($request->validated()); return new PostResource($post); } public function destroy(Post $post): Response { $post->delete(); return response()->noContent(); } }
PermissionSeeder
For every resource we create a seeder that stores a permission for each Controller action:
<?php namespace App\Modules\Posts\Seeders; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Seeder; use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; use Spatie\Permission\PermissionRegistrar; class PostPermissionSeeder extends Seeder { private null|Model|Role $defaultRole; public function run(): void { app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->createDefaultRole(); $this->createPermissions(); } private function createDefaultRole(): void { $defaultRoleName = config('crudgenerator.default_role_name'); $defaultGuardName = config('crudgenerator.default_guard_name'); $this->defaultRole = Role::query()->firstOrCreate(['name' => $defaultRoleName], ['guard_name' => $defaultGuardName]); $permission = Permission::query()->firstOrCreate(['name' => $defaultRoleName]); $this->defaultRole->givePermissionTo($permission); } private function createPermissions(): void { $viewAny = Permission::query()->firstOrCreate(['name' => 'post.viewAny']); $view = Permission::query()->firstOrCreate(['name' => 'post.view']); $create = Permission::query()->firstOrCreate(['name' => 'post.create']); $update = Permission::query()->firstOrCreate(['name' => 'post.update']); $delete = Permission::query()->firstOrCreate(['name' => 'post.delete']); $this->defaultRole->givePermissionTo($viewAny, $view, $create, $update, $delete); } }
ControllerAuthorizationTest
The ControllerAuthorizationTest test every permission for an allowed user and a restricted user. In this way we are sure every Controller actions can be finegrained assigned to any future role.
<?php namespace App\Modules\Posts\Tests\Http\Controllers; use App\Models\User; use App\Modules\Posts\Factories\PostFactory; use App\Modules\Posts\Models\Post; use App\Modules\Posts\Seeders\PostPermissionSeeder; use Database\Factories\UserFactory; use Illuminate\Foundation\Testing\RefreshDatabase; use PHPUnit\Framework\Attributes\DataProvider; use Spatie\Permission\Models\Role; use Symfony\Component\HttpFoundation\Response; use Tests\TestCase; class PostControllerAuthorizationTest extends TestCase { use RefreshDatabase; public function userWithRole(string $roleName, array $attributes = []): User { (new PostPermissionSeeder)->run(); $defaultGuardName = config('crudgenerator.default_guard_name'); $role = Role::query()->firstOrCreate(['name' => $roleName], ['guard_name' => $defaultGuardName]); return UserFactory::new()->create($attributes)->assignRole($role); } #[DataProvider('roleDataProvider')] public function testListPostAuthorization(string $role, bool $allow): void { $user = $this->userWithRole($role); $response = $this->actingAs($user)->get('posts'); $this->assertEquals($allow, $user->can('viewAny', Post::class)); $this->assertEquals($allow, $response->getStatusCode() !== Response::HTTP_FORBIDDEN, $response->getStatusCode()); } #[DataProvider('roleDataProvider')] public function testCreatePostAuthorization(string $role, bool $allow): void { $user = $this->userWithRole($role); $response = $this->actingAs($user)->post('posts'); $this->assertEquals($allow, $user->can('create', Post::class)); $this->assertEquals($allow, $response->getStatusCode() !== Response::HTTP_FORBIDDEN, $response->getStatusCode()); } #[DataProvider('roleDataProvider')] public function testUpdatePostAuthorization(string $role, bool $allow): void { $user = $this->userWithRole($role); $post = PostFactory::new()->create(); $response = $this->actingAs($user)->patch("posts/{$post->id}"); $this->assertEquals($allow, $user->can('update', $post)); $this->assertEquals($allow, $response->getStatusCode() !== Response::HTTP_FORBIDDEN, $response->getStatusCode()); } #[DataProvider('roleDataProvider')] public function testShowPostAuthorization(string $role, bool $allow): void { $user = $this->userWithRole($role); $post = PostFactory::new()->create(); $response = $this->actingAs($user)->get("posts/{$post->id}"); $this->assertEquals($allow, $user->can('view', $post)); $this->assertEquals($allow, $response->getStatusCode() !== Response::HTTP_FORBIDDEN, $response->getStatusCode()); } #[DataProvider('roleDataProvider')] public function testDeletePostAuthorization(string $role, bool $allow): void { $user = $this->userWithRole($role); $post = PostFactory::new()->create(); $response = $this->actingAs($user)->delete("posts/{$post->id}"); $this->assertEquals($allow, $user->can('delete', $post)); $this->assertEquals($allow, $response->getStatusCode() !== Response::HTTP_FORBIDDEN, $response->getStatusCode()); } public static function roleDataProvider(): array { return [ 'default role with permissions is allowed' => [ 'role' => 'admin', 'allow' => true, ], 'custom role without permissions is not allowed' => [ 'role' => 'not-allowed', 'allow' => false, ], ]; } }
Documentation
https://axyr.gitbook.io/laravel-tractor
Quick start
Run the composer install command from the terminal:
composer require axyr/laravel-tractor
Then you can generate a Module for a Model resource:
php artisan crud:generate Post -m
After that you can run the tests:
php artisan test
From here, you can start extending the module with your specific business cases.
For further information and customisation, visit our documentation page: