定制 axyr/laravel-tractor 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

axyr/laravel-tractor

Composer 安装命令:

composer require axyr/laravel-tractor

包简介

Opiniated Laravel CRUD generator for a Module/Repository/Filter based CRUD setup.

README 文档

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.

Latest Version on Packagist Tests

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,
            ],
        ];
    }
}

Quick start

Run the composer install command from the terminal:

composer require axyr/laravel-tractor

Add this snippet to the "extra" key in your composer file:

"extra": {
    "merge-plugin": {
        "merge-extra": true,
        "merge-extra-deep": true,
        "include": [
            "app-modules/*/composer.json"
        ]
    }
}

The include the package test, add the testsuite to the phpunit.xml file:

<testsuite name="Modules">
    <directory>./app-modules/*/tests</directory>
</testsuite>

This will enable the autoload of modules in a custom directory outside app.

Then you can generate a Module for a Model resource:

php artisan tractor:generate Post

After that you can run the tests:

php artisan test

From here, you can start extending the module with your specific business cases.

axyr/laravel-tractor 适用场景与选型建议

axyr/laravel-tractor 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1.32k 次下载、GitHub Stars 达 5, 最近一次更新时间为 2024 年 10 月 13 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「json」 「api」 「module」 「boilerplate」 「crud」 「laravel」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 axyr/laravel-tractor 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 axyr/laravel-tractor 我们能提供哪些服务?
定制开发 / 二次开发

基于 axyr/laravel-tractor 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1.32k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 5
  • 点击次数: 10
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 5
  • Watchers: 1
  • Forks: 3
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2024-10-13