kenokokoro/laravel-basetree

构建laravel应用时的基础扩展结构

v5.0.0 2021-06-08 09:39 UTC

README

CircleCI

支持

安装

composer require kenokokoro/laravel-basetree

将包拉入后,在您的 AppServiceProvider 中注册服务提供者

    public function register() 
    {
        $this->app->register(BaseTree\Providers\BaseTreeServiceProvider::class);
    }

这就完成了。

描述

基本的多层结构,主要用于RESTful API和Web CRUD资源。包括基础类,通过扩展这些类可以实现单个资源的RESTful API端点和CRUD操作。这强制将代码分离到业务逻辑层和数据访问层,从而得到更干净的代码。

包含

  1. 通过扩展 BaseTree\Exception\LaravelHandler.phpBaseTree\Exception\LumenHandler 来处理错误

  2. 带有必需方法的基础控制器

    a.BaseTree\Controller\Laravel\JsonController.php 用于Laravel框架上的RESTful API

    b.BaseTree\Controller\Lumen\JsonController.php 用于Lumen框架上的RESTful API

    c.BaseTree\Controller\WebController.php 用于Laravel上的基于Web的CRUD操作

  3. 基础资源,这是一个为特定Model(模型)编写的业务逻辑类 BaseTree\Resources\BaseResource.php

  4. 用于http或json响应的通用分离类。

  5. 基本的模型接口,用于每个资源和数据访问层 BaseTree\Models\BaseTreeModel.php。创建的模型应该实现此模型。

  6. 数据访问层,它将模型包装在单独的仓库中,以实现更好的结构和使用现有数据库调用。每个仓库都有自己的接口和实现。

  7. 每个数据库更新都包含在事务中,因此如果您没有收到控制器的响应,并且抛出异常,则数据库中不会持久化任何内容。

  8. LaravelTestCaseLumenTestCase,它们包含许多用于使用PHPUnit框架进行集成测试的辅助工具。

用法

要求

  1. 为您的资源创建迁移和模型。注意,模型应该扩展BaseTree模型。

    因此,在您的 app\Models 目录中创建 Foo.php

        namespace App\Models;
        
        use Illuminate\Database\Eloquent\Model;
        use BaseTree\Models\BaseTreeModel;
        
        class Foo extends Model implements BaseTreeModel 
        {
            protected $fillable = ['name', 'description'];
        }

    您必须设置 $fillable 属性才能正常工作。

  2. 为创建的模型创建数据访问层。(自动创建

    例如,您可以在您的 app 文件夹下创建一个名为 DAL 的文件夹,并在其中创建一个与模型名称相同的文件夹,在这种情况下是 Foo

    因此,在 app/DAL/Foo 中创建 FooRepository.php

        namespace App\DAL\Foo;
    
        use BaseTree\Eloquent\RepositoryInterface;
    
        interface FooRepository extends RepositoryInterface 
        {
        }

    现在创建实现新创建的接口的存储库实现。

    app/DAL/Foo 中创建 EloquentFoo.php

        namespace App\DAL\Foo;
    
        use BaseTree\Eloquent\BaseEloquent;
        use App\Models\Foo;
    
        class EloquentFoo extends BaseEloquent implements FooRepository 
        {
            public function __construct(Foo $model)
            {
                parent::__construct($model);
            }
        }

    现在在您自定义创建的服务提供者中绑定一切。我会这样做:

    app/DAL 中创建 DalServiceProvider.php

        namespace App\DAL;
    
        use Illuminate\Support\ServiceProvider;
        use App\DAL\Foo\FooRepository;
        use App\DAL\Foo\EloquentFoo;
        
        class DalServiceProvider extends ServiceProvider 
        {
            public function register() {
                $bindings = [
                    FooRepository::class => EloquentFoo::class,
                    # Every other repository should be registered here
                ];
                
                foreach($bindings as $interface => $implementation) {
                    $this->app->bind($interface, $implementation);
                }
            }
        }

    然后,在您的 AppServiceProvider 中注册 DalServiceProvider

        use App\DAL\DalServiceProvider;
    
        ...
        public function register() {
            $this->app->register(DalServiceProvider::class);
        }

    这样,您就完成了数据访问层结构。您可以在需要时注入接口并重用查询。

  3. 创建专门负责业务逻辑规则和与数据访问层交互的资源。在您的 app 文件夹中创建一个名为 ResourcesBLL 的文件夹,并在其中保存您的模型资源。(自动创建

    app/BLL 目录下创建文件 FooResource.php

        namespace App\BLL;
        
        use BaseTree\BLL\BaseResource
        use App\DAL\FooRepository;
        
        class FooResource extends BaseResource
        {
            public function __construct(FooRepository $repository) 
            {
                parent::__construct($repository);
            }
        }

    这样基本上资源就创建完成了,但此包包含一些辅助接口,可以帮助您组织验证、创建和更新。

    BaseTree\Resources\Contracts\ResourceValidation 接口将强制您实现 storeRules()updateRules()destroyRules()。在资源上实现此接口后,对 store()update()destroy() 的请求将进行验证。如果您不需要对某些方法进行验证,只需返回空数组即可。您也可以根据需要单独使用它们(检查 BaseTree\Resources\Contracts\ResourceValidation,所有扩展接口都有自己的方法,如果需要可以单独使用)

    BaseTree\Resources\Contracts\ResourceCallbacks 接口包含 created()updated() 方法,它们基本上是在资源创建或更新后的钩子。传递的 $dependencyAttributes 值包含除了设置在您的模型上的 $fillable 属性之外的所有内容。这很好,因为使用这些值您可以轻松地更新您的关联。如果需要,回调也可以单独使用,就像上面提到的资源验证一样(检查 BaseTree\Resources\Contracts\ResourceCallbacks 的扩展接口)

    $attributes 包含 $fillable 属性。

  4. 现在来配置控制器。每个控制器应该扩展 BaseTree\Controllers\Laravel\JsonController(用于laravel)和 BaseTree\Controllers\Lumen\JsonController(用于lumen),当使用json时,根据需要扩展 BaseTree\Controllers\Laravel\WebController。(自动创建

    app/Http/Controllers(或其它位置)创建 FoosController.php

        namespace App\Http\Controllers;
        
        use BaseTree\Controllers\Laravel\JsonController;
        use App\BLL\FooResource;
        
        class FoosController extends JsonController 
        {
            public function __construct(FooResource $resource) 
            {
                parent::__construct($resource);
            }
        }
  5. 现在您可以准备您的路由

        Route::resource('foos', 'FoosController')->except(['edit']);
  6. 使用 base-tree 的异常处理器。在您的 App\Exceptions\Handler 中执行以下操作

        namespace App\Exceptions;
    
        use BaseTree\Exception\LaravelHandler as BaseTreeHandler;
    
        class Handler extends BaseTreeHandler
        {
        }

请求 - 响应

请注意,您必须始终将 Accept 头设置为 application/json

有了这些,现在您只需创建几个类即可拥有 RESTful API

  1. 访问 /api/foos&datatable=1 将返回可以用于 jQuery datatables 插件的响应。
  2. 访问 /api/foos&paginate=1&perPage=10 将返回分页响应,其中包含下一页和上一页的 URL。默认值 perPage 为 15。
  3. 访问 api/foos&constraints[0]=name|=|bar 将返回所有名为 bar 的 foos。您可以使用 PHP 函数 http_build_query(['constraints' => ['name|=|bar', 'active|1']]) 构建查询字符串。
  4. 访问 api/foos&fields[0]=Bar 将返回所有 foos,但在响应中包含 Bar 关联。

约束值的结构为 columnName|operator|value。如果您需要根据另一个列添加约束:columnName|operator|`otherColumn`

测试

此包包含数据库测试类,可以使您的数据库所需测试变得更容易。

  1. phpunit.xml 中设置您的 DB_CONNECTION 为测试
        <env name="DB_CONNECTION" value="testing"></env>
  2. 在您的 database.php 配置文件中设置测试连接:在 config/database.php 下的 connections 添加
        'testing' => [
            'driver' => 'sqlite',
            'database' => ':memory:',
        ],
    这将使 BaseTree\Tests\Traits\DatabaseMigrations 能够在测试开始前迁移和填充,在测试完成后回滚。这样您将始终拥有干净的数据库。

测试模型

tests/Models 下创建与您的 Foo.php 模型对应的 FooTest.php。假设您的 Foo 模型有一个 Bar 模型。

    namespace Tests\Models;

    use App\Models\Foo;
    use App\Models\Bar;
    use BaseTree\Tests\LaravelDatabaseTestCase;

    class FooTest extends LaravelDatabaseTestCase
    {
        /** @test */
        public function a_foo_has_one_bar()
        {
            $foo = create(Foo::class);
            $bar = create(Bar::class);

            $foo->bar()->save($bar);

            $this->assertHasOne($foo, $bar, 'bar', ['id' => $bar->id, 'foo_id' => $foo->id]);
        }
    }

测试控制器端点

在创建扩展了 DatabaseTestCaseFoosController 之后,您可以

    use BaseTree\Responses\JsonResponse;
    use App\Models\Foo;

    ...
    
    /** @test */
    public function it_should_fetch_all_foos(): void
    {
        $response = $this->jsonGet(route("foos.index"));
        $response->assertStatus(JsonResponse::HTTP_OK)->assertJsonStructure(['status', 'message', 'data']);
    }
    
    /** @test */
    public function it_requires_data_in_order_to_store_foo(): void
    {
        $response = $this->jsonPost(route('foos.store'));
        $response->assertStatus(JsonResponse::HTTP_UNPROCESSABLE_ENTITY)->assertJsonStructure([
            'status',
            'message',
            'validator'
        ]);
        $validator = $response->json()['validator'];

        $this->assertCount(7, $validator);
        
        # Response messages assertions. Third argument is value that laravel will translate without the _
        $this->assertFieldRequired($validator, 'name');
        $this->assertEmailField($validator, 'user_email', 'user email');
        $this->assertPasswordIsConfirmed($validator, 'password');
        $this->assertValueIn($validator, 'value_from_enum', 'value from enum');
        $this->assertFieldExist($validator, 'id');
        $this->assertFieldIsArray($validator, 'array');
        $this->assertValueIsUnique($validator, 'unique_column', 'unique column');
    }
    
    /** @test */
    public function foo_can_be_stored(): void
    {
        $response = $this->jsonPost(route('foos.store', ['name' => 'Foo Name']));
        $response->assertStatus(JsonResponse::HTTP_UNPROCESSABLE_ENTITY)->assertJsonStructure([
            'status',
            'message',
            'validator'
        ]);
        
        $this->assertCreated(new Foo, ['name' => 'Foo Name']);
    }

只创建空文件感觉有限

如果您需要额外的功能,您可以始终覆盖父方法。例如,如果您想为只有名称值作为值的资源生成 slug,在您的 app\BLL\FooResource.php

    namespace App\BLL;
    
    use BaseTree\Resources\BaseResource;
    use App\DAL\FooRepository;
    
    class FooResource extends BaseResource
    {
        public function __construct(FooRepository $repository) 
        {
            parent::__construct($repository);
        }
        
        public function store(array $attributes) 
        {
            $attributes['slug'] = str_slug($attributes['name']);
            # Or whatever logic you need here
            
            return parent::store($attributes);
        }
        
    }

同样的逻辑适用于控制器和DAL。您需要定制的任何内容都可以进行扩展和覆盖。

Artisan 生成器

  1. 生成数据访问层。此命令将生成仓库和eloquent实现。不要忘记将其绑定到您的 ServiceProvider

    Usage:
      php artisan basetree:dal [options]
    
    Options:
          --model[=MODEL]                              Fully qualified model name including the namespace
          --interface-folder[=INTERFACE-FOLDER]        Folder where to create the DAL interface [default: "app/DAL/[model-name]"]
          --interface-namespace[=INTERFACE-NAMESPACE]  Namespace to create the DAL interface under [default: "App\DAL\[model-name]"]
          --dal-folder[=DAL-FOLDER]                    Folder where to create the DAL implementation [default: "app/DAL/[model-name]"]
          --dal-namespace[=DAL-NAMESPACE]              Namespace to create the DAL implementation under [default: "App\DAL\[model-name]"]

    示例: php artisan basetree:dal --model=App\\Models\\User

  2. 生成业务逻辑层。这将生成一个资源,在构造函数中注入了仓库接口

    Usage:
      php artisan basetree:bll [options]
    
    Options:
          --model[=MODEL]                  Fully qualified model name including namespace
          --dal-interface[=DAL-INTERFACE]  Fully qualified data access layer name including namespace
          --folder[=FOLDER]                Folder where to create the BLL [default: "app/BLL/"]
          --namespace[=NAMESPACE]          Namespace to create the BLL under [default: "App\BLL"]

    示例: php artisan basetree:bll --model=App\\Models\\User --dal-interface=App\\DAL\\User\\UserRepository

  3. 生成控制器。生成的控制器将在构造函数中注入指定的业务逻辑层

    注意:在此阶段,生成器仅创建扩展自 RestfulJsonController 的控制器。您需要手动更改生成控制器以扩展 WebController 或手动创建它

    Usage:
      php artisan basetree:controller [options]
    
    Options:
          --model-plural[=MODEL-PLURAL]  Plural form of the model name. For instance if the model is User, you should send here Users
          --bll[=BLL]                    Fully qualified business logic layer name including namespace
          --folder[=FOLDER]              Folder where to create the controller [default: "app/Http/Controllers/Api/"]
          --namespace[=NAMESPACE]        Namespace to create the controller under [default: "App\Http\Controllers\Api"]

    示例: php artisan basetree:controller --model-plural=users -bll=App\\BLL\\UserResource

  4. 发布docker-compose架构。查看.env.docker-compose.example以获取使docker容器正常运行所需的变量。

    Usage:
      php artisan basetree:boilerplates [options]
    
    Options:
          --docker-compose  Publish the docker structure
    
    Help:
      Publish some already predefined environments.
          --docker-compose: Docker environment for local development (nginx 1.13, php7.1-fpm + composer, npm 3.3, nodejs 6.7, MariaDB 10.3, phpmyadmin 4.7)

    示例: php artisan basetree:boilerplates --docker-compose

    所需变量

    • DOCKER_HOST_UID=1000 # 您的主机用户ID。通过执行 echo $UIDid username 检查它。
    • DOCKER_HOST_GID=1000 # 您的主机组ID。通过执行 echo $GIDid username 检查它。
    • DATABASE_LOCAL_STORAGE=/opt/mariadb/project # 在您的主机机器上保存数据库文件的位置。这是必需的,因为如果运行 docker-compose down -v,这将销毁数据库中的数据,如果存储未挂载在您的主机机器上。
    • PMA_PORT=81 # PhpMyAdmin的公共暴露端口。如果运行在QA环境中或不需要,您可以将其从 docker-compose.yml 文件中删除,或通过运行 docker-compose stop phpmyadmin 来删除。
    • NGINX_SERVER_NAME=localhost # 虚拟主机名称
    • NGINX_PORT=80 # nginx容器的公共暴露端口。为了在域名后不添加端口号,应为 80。例如:如果您的 NGINX_PORT=8080,您将不得不在浏览器中访问它:localhost:8080。另外,如果您已经有一些服务在监听给定的端口,您将不得不关闭它们。
    • CONTAINER_ROOT=/application # 在容器内您的项目的名称。
    • DB_ROOT_PASSWORD=root-pass123 # MariaDB根密码
    • QA_HTTP_HOST= # 如果您正在运行多个docker实例,并且希望将它们全部绑定到 80 端口,您将必须在此处指定 fastcgi_param HTTP_HOST,以便您的应用程序重定向到您的代理URL。
    • DB_HOST=mariadb # 如果您正在使用docker mariadb实例,而不是您已安装的自己的。

    设置好所有这些后,您必须运行 docker-compose -f docker-compose.yml -f qa.docker-compose.yml -f dev.docker-compose.yml up -d 并等待构建完成。通过运行 docker-compose ps 检查容器状态。您应该看到类似以下的内容

            Name               Command                          State               Ports                
    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------
    tutorial_app_1         docker-php-entrypoint /sta ...       Up           443/tcp, 0.0.0.0:80->80/tcp, 9000/tcp
    tutorial_mariadb_1     docker-entrypoint.sh mysqld          Up           0.0.0.0:3307->3306/tcp               
    tutorial_phpmyadmin_1  /run.sh phpmyadmin                   Up           0.0.0.0:81->80/tcp 

Lumen支持

关于Laravel的相同说明也适用于Lumen,其行为应与Lumen和Laravel相同。Lumen的特殊情况包括

  1. Lumen控制器与Laravel控制器不同

        namespace App\Http\Controllers;
        
        use BaseTree\Controllers\Lumen\JsonController;
        use App\BLL\FooResource;
        
        class FoosController extends JsonController 
        {
            public function __construct(FooResource $resource) 
            {
                parent::__construct($resource);
            }
        }
  2. 使用 base-tree 的异常处理器。在您的 App\Exceptions\Handler 中执行以下操作

        namespace App\Exceptions;
    
        use BaseTree\Exception\LumenHandler as BaseTreeHandler;
    
        class Handler extends BaseTreeHandler
        {
        }
  3. 数据库需要测试在 tests/Models 中创建 FooTest.php,它与您的 Foo.php 模型相对应。假设您的 Foo 模型有一个 Bar 模型。

        namespace Tests\Models;
    
        use App\Models\Foo;
        use App\Models\Bar;
        use BaseTree\Tests\LumenDatabaseTestCase;
    
        class FooTest extends LumenDatabaseTestCase
        {
            /** @test */
            public function a_foo_has_one_bar()
            {
                $foo = create(Foo::class);
                $bar = create(Bar::class);
    
                $foo->bar()->save($bar);
    
                $this->assertHasOne($foo, $bar, 'bar', ['id' => $bar->id, 'foo_id' => $foo->id]);
            }
        }

待办事项

  1. 测试一切
  2. Artisan生成器
  3. 维基示例和说明
  4. 包含JWT支持
  5. 添加Artisan单端点生成器,一次封装所有生成器

测试

make phpunit 执行测试

许可

BaseTree软件包是开源软件,许可协议为MIT许可