-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
60 changed files
with
452 additions
and
10,123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,349 @@ | ||
# 📨 Another Laravel CRUD Generator | ||
# 🚜 Laravel Tractor - Another Laravel API Module Generator | ||
|
||
Another Laravel CRUD generator. | ||
Scaffold a Laravel module structure for JSON API's. | ||
|
||
This generator only generates the PHP files for a route model based resource controllers. | ||
![](docs/img/tractor.jpg) | ||
|
||
Not frontend, no UI, just JSON based backend code. | ||
## Introduction | ||
|
||
## Repository and QueryFilter based controllers | ||
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. | ||
|
||
This package is a bit different, as it will include Repository and QueryFilter classes for each CRUD. | ||
[![Latest Version on Packagist](https://img.shields.io/packagist/v/axyr/tractor.svg?style=flat-square)](https://packagist.org/packages/axyr/tractor) [![Tests](https://github.com/axyr/laravel-tractor/actions/workflows/run-tests.yml/badge.svg)](https://github.com/axyr/laravel-tractor/actions/workflows/run-tests.yml) | ||
|
||
The Repositories are very simple classes that wire up the QueryFilters with the Controllers. | ||
After installing, you can generate all required files for a working CRUD based module: | ||
|
||
The QueryFilters are based on the outstanding Laracasts tutorial: | ||
```shell | ||
php artisan tractor:generate Post | ||
``` | ||
|
||
https://laracasts.com/series/eloquent-techniques/episodes/4 | ||
Above command will generate a basic module structure with all the files needed for a model based JSON API. | ||
|
||
THe biggest difference with this implementation of the Dedicated Query filters, is that we use a plain array instead of the Request object. | ||
This makes it easier to reuse the Filters in Jobs, where you don't want to serialize the Request object. | ||
The module will have boilerplate tests that tests all the permission authorization, database operations and filters. | ||
|
||
https://github.com/axyr/laravel-query-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 | ||
``` | ||
|
||
## Generated files | ||
## Examples | ||
|
||
The generate command will generate the following files: | ||
A full example of a generater module can be found here: | ||
|
||
- Model | ||
- Controller | ||
- Resources | ||
- Requests | ||
- Policy | ||
- Factory | ||
- migration | ||
- Repository | ||
- Filter | ||
- ControllerTest | ||
- ControllerAuthorisationTest | ||
- FilterTest | ||
TODO | ||
|
||
## Modules | ||
We generate the bare minimum, but workable and testable code, some examples: | ||
|
||
By Default the files will be installed in a app-modules directory. | ||
### Model | ||
|
||
The philosophy here is that with Repositories, Filters and Policy classes, we are highly likely creating an above average complex application, | ||
where we want strict separation of concerns from the start and not only groupd files by type only. | ||
```php | ||
<?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 | ||
<?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 | ||
<?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 | ||
<?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](https://axyr.gitbook.io/laravel-tractor) | ||
|
||
## Quick start | ||
|
||
|
||
Run the composer install command from the terminal: | ||
|
||
```shell | ||
composer require axyr/laravel-tractor | ||
``` | ||
|
||
Then you can generate a Module for a Model resource: | ||
|
||
```shell | ||
php artisan crud:generate Post -m | ||
``` | ||
|
||
After that you can run the tests: | ||
|
||
```shell | ||
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: | ||
|
||
[https://axyr.gitbook.io/laravel-tractor](https://axyr.gitbook.io/laravel-tractor) |
Oops, something went wrong.