发布于 2026-01-06 4 阅读
0

模块化 Laravel

模块化 Laravel

介绍

以下 Laravel 项目/目录结构代表了我创建新 Laravel 项目时经常使用的个人模块化/SOA 结构样板。

在过去的几个月里,我发现自己多次创建了相同的结构,所以我决定创建一个项目启动模板。

核心结构

核心模块包含主要接口、抽象类和实现。

目录概览

app
├── Modules
│   └── Core
│       ├── Controllers
│       |   ├── ApiController.php
|       |   └── Controller.php
│       ├── Exceptions
│       |   ├── FormRequestTableNotFoundException.php
│       |   ├── GeneralException.php
│       |   ├── GeneralIndexException.php
│       |   ├── GeneralSearchException.php
│       |   ├── GeneralStoreException.php
│       |   ├── GeneralNotFoundException.php
│       |   ├── GeneralDestroyException.php
|       |   └── GeneralUpdateException.php
│       ├── Filters
│       |   ├── QueryFilter.php
|       |   └── FilterBuilder.php
│       ├── Helpers
|       |   └── Helper.php
│       ├── Interfaces
│       |   ├── FilterInterface.php
│       |   ├── SearchInterface.php
|       |   └── RepositoryInterface.php
│       ├── Models
|       |   └── .gitkeep
│       ├── Repositories
|       |   └── Repository.php
│       ├── Requests
│       |   ├── FormRequest.php
│       |   ├── CreateFormRequest.php
│       |   ├── DeleteFormRequest.php
│       |   ├── SearchFormRequest.php
│       |   ├── UpdateFormRequest.php
|       |   └── ShowFormRequest.php
│       ├── Resources
│       |   └── .gitkeep 
│       ├── Scopes
|       |   └── .gitkeep
│       ├── Traits
│       |   ├── ApiResponses.php
|       |   └── Filterable.php
│       ├── Transformers
│       |   ├── EmptyResource.php
|       |   └── EmptyResourceCollection.php
│       └── 
└── 
Enter fullscreen mode Exit fullscreen mode

接口

主要接口是RepositoryInterface,它定义了基本的 CRUD 和一些附加方法。


namespace App\Modules\Core\Interfaces;

interface RepositoryInterface
{
    /**
     * @return mixed
     */
    public function findAll();

    /**
     * @param int $id
     * @return mixed
     */
    public function findById(int $id);

    /**
     * @param string $column
     * @param $value
     * @return mixed
     */
    public function findBy(string $column, $value);

    /**
     * @param array $data
     * @return mixed
     */
    public function create(array $data);

    /**
     * @param int $id
     * @param array $data
     * @return mixed
     */
    public function update(int $id, array $data);


    /**
     * @param int $id
     * @return mixed
     */
    public function delete(int $id);

}
Enter fullscreen mode Exit fullscreen mode

实现了RepositoryInterface 接口的Repository如下所示:


namespace App\Modules\Core\Repositories;

use App\Modules\Core\Interfaces\RepositoryInterface;

class Repository implements RepositoryInterface
{
    /**
     * Model::class
     */
    public $model;

    /**
     * @return mixed
     */
    public function findAll()
    {
        return $this->model::all();
    }

    /**
     * @param int $id
     * @return mixed
     */
    public function findById(int $id)
    {
        return $this->model::find($id);
    }

    /**
     * @param string $column
     * @param $value
     * @return mixed
     */
    public function findBy(string $column, $value)
    {
        return $this->model::where($column, $value);
    }

    /**
     * @param array $data
     * @return mixed
     */
    public function create(array $data)
    {
        return $this->model::create($data)->fresh();
    }

    /**
     * @param int $id
     * @param array $data
     * @return mixed
     */
    public function update(int $id, array $data)
    {
        $item = $this->findById($id);
        $item->fill($data);
        $item->save();
        return $item->fresh();
    }

    /**
     * @param int $id
     * @return mixed|void
     */
    public function delete(int $id)
    {
        $this->model::destroy($id);
    }
}
Enter fullscreen mode Exit fullscreen mode

另外两个接口分别是搜索接口筛选接口。

SearchInterface定义了一个方法,当从数据库检索数据时需要搜索过滤器时,每个模块的特定存储库类可以实现此接口


namespace App\Modules\Core\Interfaces;

interface SearchInterface
{
    /**
     * @param array $request
     * @return mixed
     */
    public function search(array $request);
}
Enter fullscreen mode Exit fullscreen mode

SearchInterface的示例实现

namespace App\Modules\Example\Repositories;

class ExampleRepository extends Repository implements ExampleInterface, SearchInterface
{
    /**
     * @var string
     */
    public $model = Example::class;

    /**
     * @param array $request
     * @return mixed
     * @throws ExampleSearchException
     */
    public function search(array $request)
    {
        try {
            $query = $this->model::filterBy($request);

            $query->orderBy(Arr::get($request, 'order_by') ?? 'id', Arr::get($request, 'sort') ?? 'desc');

            return $query->paginate(Arr::get($request, 'per_page') ?? (new $this->model)->getPerPage());

        } catch (Exception $exception) {
            throw new ExampleSearchException($exception);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

这还可以进一步抽象化,但我会在未来的版本中处理这个问题😄

此外,FilterInterface只定义了一个方法,如果需要按特定请求键进行过滤,则每个模块的每个 Filter 类都会实现此接口。


namespace App\Modules\Core\Interfaces;

interface FilterInterface
{
    /**
     * @param $value
     * @return mixed
     */
    public function handle($value);
}
Enter fullscreen mode Exit fullscreen mode

FilterInterface的示例实现


namespace App\Modules\Example\Filters;

use App\Modules\Core\Filters\QueryFilter;
use App\Modules\Core\Interfaces\FilterInterface;

class Name extends QueryFilter implements FilterInterface
{
    /**
     * @param $value
     * @return mixed|void
     */
    public function handle($value)
    {
        $this->query->where('name', 'like', '%' . $value . '%');
    }
}
Enter fullscreen mode Exit fullscreen mode

例外情况

Exceptions目录包含通用异常,其中包含一些预定义的$code$message,当每个模块的自定义异常扩展通用异常时,这些预定义值可以被覆盖。

例如,在提供的模块示例中,定义了多个异常。

ExampleNotFoundException


namespace App\Modules\Example\Exceptions;

use App\Modules\Core\Exceptions\GeneralNotFoundException;

class ExampleNotFoundException extends GeneralNotFoundException
{

}

Enter fullscreen mode Exit fullscreen mode

这扩展了GeneralNotFoundException异常。


namespace App\Modules\Core\Exceptions;

class GeneralNotFoundException extends GeneralException
{
    public $code = 404;

    /**
     * @return string|null
     */
    public function message(): ?string
    {
        return "The requested resource was not found in the database";
    }
}

Enter fullscreen mode Exit fullscreen mode

请求

Requests目录包含通用表单请求抽象类。

FormRequest类重写了src/Illuminate/Foundation/Http/FormRequest.php中的failedValidation方法。


abstract class FormRequest extends LaravelFormRequest
{
    /**
     * Handle a failed validation attempt.
     *
     * @param Validator $validator
     * @return void
     *
     */
    protected function failedValidation(Validator $validator)
    {
        $errors = (new ValidationException($validator))->errors();

        throw new HttpResponseException(
            response()->json(['errors' => $errors], Response::HTTP_UNPROCESSABLE_ENTITY)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

然后,其他每个抽象表单请求类都继承自这个抽象表单请求类。

创建表单请求


namespace App\Modules\Core\Requests;

abstract class CreateFormRequest extends FormRequest
{
    protected $table = '';

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    abstract public function rules();
}
Enter fullscreen mode Exit fullscreen mode

现在,当抽象类CreateFormRequest在模块中被扩展时,扩展该类的类必须实现抽象方法rules(),其中定义了验证规则。

创建示例请求


namespace App\Modules\Example\Requests;

use App\Modules\Core\Requests\CreateFormRequest;
use Illuminate\Validation\Rule;

class CreateExampleRequest extends CreateFormRequest
{
    protected $table = 'examples';

    /**
     * @inheritDoc
     */
    public function rules(): array
    {
        return [
            'name' => [
                'required',
                'string',
                Rule::unique($this->table, 'name')
            ],
            'example_type_id' => [
                'required',
                Rule::exists('example_types', 'id')
            ],
            'is_active' => [
                'sometimes',
                'boolean'
            ],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

特征

Traits目录包含模块中使用的核心 trait。ApiResponses trait 定义了错误和成功响应的 JSON 默认结构

其中一些方法已在那里定义。


    /**
     * @param Exception $exception
     * @param array $data
     * @param string $title
     * @return JsonResponse
     */
    public function exceptionRespond(Exception $exception, $data = [], $title = 'Error'): JsonResponse
    {
        return response()->json(
            [
                'title' => $title,
                'message' => $exception->getMessage(),
                'code' => $exception->getCode(),
            ],
            $exception->getCode());
    }

    /**
     * @param $data
     * @return JsonResponse
     */
    public function respond($data): JsonResponse
    {
        return response()->json(
                [
                    'message' => $this->message,
                    'code' => $this->responseCode,
                    'data' => $data
                ],
                $this->responseCode);
    }

Enter fullscreen mode Exit fullscreen mode

要了解核心结构的大致情况,请克隆此仓库或创建一个新的 Composer 项目。

git clone https://github.com/keljtanoski/modular-laravel.git
Enter fullscreen mode Exit fullscreen mode
composer create-project keljtanoski/modular-laravel
Enter fullscreen mode Exit fullscreen mode

示例模块结构

这是一个可直接使用的示例模块。该模块的主要目的是演示接口、存储库和服务之间的交互,它可以轻松复制,只需简单的查找和替换操作,即可快速搭建并运行新的模块。

目录概览

app
├── Modules
│   └── Example
│       ├── Config
|       |   └── .gitkeep
│       ├── Controllers
│       │   ├── Api
│       │   │   └── ExamplesController.php
|       |   └── ExamplesController.php
│       ├── Exceptions
│       |   ├── ExampleDestroyException.php
│       |   ├── ExampleIndexException.php
│       |   ├── ExampleNotFoundException.php
│       |   ├── ExampleSearchException.php
│       |   ├── ExampleStoreException.php
|       |   └── ExampleUpdateException.php
│       ├── Filters
│       |   ├── ExampleType.php
│       |   ├── ExampleTypeId.php
│       |   ├── IsActive.php
|       |   └── Name.php
│       ├── Helpers
|       |   └── .gitkeep
│       ├── Interfaces
|       |   └── ExampleInterface.php
│       ├── Models
|       |   └── Example.php
│       ├── Repositories
|       |   └── ExampleRepository.php
│       ├── Requests
│       |   ├── CreateExampleRequest.php
│       |   ├── DeleteExampleRequest.php
│       |   ├── SearchExampleRequest.php
│       |   ├── ShowExampleRequest.php
|       |   └── UpdateExampleRequest.php
│       ├── Resources
│       |   ├── lang
|       |   |   └── .gitkeep
│       |   └── views
|       |       ├── layouts
|       |       |   └── master.blade.php
|       |       ├── index.blade.php
|       |       └── create.blade.php
│       ├── routes
│       |   ├── api.php
|       |   └── web.php
│       ├── Services
|       |   └── ExampleService.php
│       ├── Traits
|       |   └── .gitkeep
│       ├── Transformers
|       |   └── ExampleResource.php
│       └──
└── 
Enter fullscreen mode Exit fullscreen mode

控制器

Controllers目录存放着模块的控制器。ExamplesController用于 Web 端点,而Api /ExamplesController用于 API 端点

API/示例控制器

exampleService通过构造函数注入。 该服务负责将所需的操作委托给实现了ExampleInterface接口Repository 类。


namespace App\Modules\Example\Controllers\Api;

class ExamplesController extends ApiController
{
    /**
     * @var ExampleService
     */
    protected $exampleService;

    /**
     * @param ExampleService $exampleService
     */
    public function __construct(ExampleService $exampleService)
    {
        $this->exampleService = $exampleService;
    }
Enter fullscreen mode Exit fullscreen mode
index()方法
    /**
     * @param SearchExampleRequest $request
     * @return AnonymousResourceCollection
     * @throws ExampleIndexException
     */
    public function index(SearchExampleRequest $request)
    {
        try {
            return ExampleResource::collection($this->exampleService->search($request->validated()));
        } catch (Exception $exception) {
            throw new ExampleIndexException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode
show()方法
    /**
     * @param ShowExampleRequest $request
     * @return JsonResponse
     * @throws ExampleNotFoundException
     */
    public function show(ShowExampleRequest $request)
    {
        try {
            return $this
                ->setMessage(__('apiResponse.ok',
                    ['resource' => Helper::getResourceName(
                        $this->exampleService->exampleRepository->model)
                    ]))
                ->respond(new ExampleResource($this->exampleService->getById($request->id)));
        } catch (Exception $exception) {
            throw new ExampleNotFoundException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode
store()方法
    /**
     * @param CreateExampleRequest $request
     * @return JsonResponse
     * @throws ExampleStoreException
     */
    public function store(CreateExampleRequest $request)
    {
        try {
            return $this
                ->setMessage(__('apiResponse.storeSuccess',
                    ['resource' => Helper::getResourceName(
                        $this->exampleService->exampleRepository->model)
                    ]))
                ->respond(new ExampleResource($this->exampleService->create($request->validated())));
        } catch (Exception $exception) {
            throw new ExampleStoreException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode
update()方法
    /**
     * @param UpdateExampleRequest $request
     * @return JsonResponse
     * @throws ExampleUpdateException
     */
    public function update(UpdateExampleRequest $request)
    {
        try {
            return $this
                ->setMessage(__('apiResponse.updateSuccess',
                    ['resource' => Helper::getResourceName(
                        $this->exampleService->exampleRepository->model)
                    ]))
                ->respond(new ExampleResource($this->exampleService
                    ->update($request->validated())
                ));
        } catch (Exception $exception) {
            throw new ExampleUpdateException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode
destroy()方法

    /**
     * @param DeleteExampleRequest $request
     * @return JsonResponse
     * @throws ExampleDestroyException
     */
    public function destroy(DeleteExampleRequest $request)
    {
        try {
            return $this
                ->setMessage(__('apiResponse.deleteSuccess',
                    ['resource' => Helper::getResourceName(
                        $this->exampleService->exampleRepository->model)
                    ]))
                ->respond($this->exampleService->delete($request->id));
        } catch (Exception $exception) {
            throw new ExampleDestroyException($exception);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

示例控制器

WEB-ExamplesController 具有标准方法:

  • index() - 返回所有资源
  • create() - 返回用于创建资源的视图
  • store() - 存储创建表单中的数据
  • edit() - 返回用于编辑资源的视图
  • update() - 使用表单数据更新资源
  • destroy() - 销毁资源

构造函数中使用并注入了相同的ExampleService 。

namespace App\Modules\Example\Controllers;

class ExamplesController extends Controller
{
    /**
     * @var ExampleService
     */
    protected $exampleService;

    /**
     * @param ExampleService $exampleService
     */
    public function __construct(ExampleService $exampleService)
    {
        $this->exampleService = $exampleService;
    }

    /**
     * Display a listing of the resource.
     * @return Renderable
     */
    public function index()
    {
        return view('Example::index');
    }

    public function create()
    {
        return view("Example::create");
    }
}

Enter fullscreen mode Exit fullscreen mode

服务

Services目录包含模块中使用的服务类。

ExampleInterface通过构造函数注入,该接口通过RepositoryServiceProvider绑定到实现


class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * @var string[]
     */
    protected $repositories = [
        ExampleInterface::class => ExampleRepository::class,
        ExampleTypeInterface::class => ExampleTypeRepository::class,
    ];

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        foreach ($this->repositories as $interface => $repository) {
            $this->app->bind($interface, function ($app) use ($repository) {
                return new $repository;
            });
        }
    }
Enter fullscreen mode Exit fullscreen mode

该模块中的ExampleService结构如下:


namespace App\Modules\Example\Services;

class ExampleService
{
    public $exampleRepository;

    public function __construct(ExampleInterface $exampleRepository)
    {
        $this->exampleRepository = $exampleRepository;
    }

Enter fullscreen mode Exit fullscreen mode

ExampleService中已定义的方法如下:

getById()方法


    /**
     * @param int $id
     * @return mixed
     * @throws ExampleNotFoundException
     */
    public function getById(int $id)
    {
        try {
            return $this->exampleRepository->findById($id);
        } catch (Exception $exception) {
            throw new ExampleNotFoundException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode

getAll()方法


    /**
     * @return mixed
     * @throws ExampleIndexException
     */
    public function getAll()
    {
        try {
            return $this->exampleRepository->findAll();
        } catch (Exception $exception) {
            throw new ExampleIndexException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode

create()方法


    /**
     * @param array $data
     * @return mixed
     * @throws ExampleStoreException
     */
    public function create(array $data)
    {
        try {
            return $this->exampleRepository->create($data);
        } catch (Exception $exception) {
            throw new ExampleStoreException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode

update()方法


    /**
     * @param array $data
     * @return mixed
     * @throws ExampleUpdateException
     */
    public function update(array $data)
    {
        try {
            return $this->exampleRepository->update($data['id'], $data);
        } catch (Exception $exception) {
            throw new ExampleUpdateException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode

delete()方法


    /**
     * @param int $id
     * @return mixed|void
     * @throws ExampleDestroyException
     */
    public function delete(int $id)
    {
        try {
            return $this->exampleRepository->delete($id);
        } catch (Exception $exception) {
            throw new ExampleDestroyException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode

search()方法


/**
     * @param array $data
     * @return mixed|void
     * @throws ExampleSearchException
     */
    public function search(array $data)
    {
        try {
            return $this->exampleRepository->search($data);
        } catch (Exception $exception) {
            throw new ExampleSearchException($exception);
        }
    }
Enter fullscreen mode Exit fullscreen mode

要了解模块结构的大致情况,请克隆仓库或创建一个新的 Composer 项目。

git clone https://github.com/keljtanoski/modular-laravel.git
Enter fullscreen mode Exit fullscreen mode
composer create-project keljtanoski/modular-laravel
Enter fullscreen mode Exit fullscreen mode

路线列表

这只是php artisan route:list命令的输出结果。


+----------+---------------------------+---------------------------+------------------------------------------------------------------------+---------------+
| Method   | URI                       | Name                      | Action                                                                 | Middleware    |
+----------+---------------------------+---------------------------+------------------------------------------------------------------------+---------------+
| POST     | api/v1/example-types      | api.example_types.store   | App\Modules\ExampleType\Controllers\Api\ExampleTypesController@store   | api           |
| GET|HEAD | api/v1/example-types      | api.example_types.index   | App\Modules\ExampleType\Controllers\Api\ExampleTypesController@index   | api           |
| DELETE   | api/v1/example-types/{id} | api.example_types.destroy | App\Modules\ExampleType\Controllers\Api\ExampleTypesController@destroy | api           |
| PATCH    | api/v1/example-types/{id} | api.example_types.update  | App\Modules\ExampleType\Controllers\Api\ExampleTypesController@update  | api           |
| GET|HEAD | api/v1/example-types/{id} | api.example_types.show    | App\Modules\ExampleType\Controllers\Api\ExampleTypesController@show    | api           |
| GET|HEAD | api/v1/examples           | api.examples.index        | App\Modules\Example\Controllers\Api\ExamplesController@index           | api           |
| POST     | api/v1/examples           | api.examples.store        | App\Modules\Example\Controllers\Api\ExamplesController@store           | api           |
| GET|HEAD | api/v1/examples/{id}      | api.examples.show         | App\Modules\Example\Controllers\Api\ExamplesController@show            | api           |
| PATCH    | api/v1/examples/{id}      | api.examples.update       | App\Modules\Example\Controllers\Api\ExamplesController@update          | api           |
| DELETE   | api/v1/examples/{id}      | api.examples.destroy      | App\Modules\Example\Controllers\Api\ExamplesController@destroy         | api           |
| POST     | example-types             | example_types.store       | App\Modules\ExampleType\Controllers\ExampleTypesController@store       | web           |
| GET|HEAD | example-types             | example_types.index       | App\Modules\ExampleType\Controllers\ExampleTypesController@index       | web           |
| GET|HEAD | example-types/create      | example_types.create      | App\Modules\ExampleType\Controllers\ExampleTypesController@create      | web           |
| GET|HEAD | example-types/{id}        | example_types.show        | App\Modules\ExampleType\Controllers\ExampleTypesController@show        | web           |
| PATCH    | example-types/{id}        | example_types.update      | App\Modules\ExampleType\Controllers\ExampleTypesController@update      | web           |
| DELETE   | example-types/{id}        | example_types.destroy     | App\Modules\ExampleType\Controllers\ExampleTypesController@destroy     | web           |
| GET|HEAD | example-types/{id}/edit   | example_types.edit        | App\Modules\ExampleType\Controllers\ExampleTypesController@edit        | web           |
| GET|HEAD | examples                  | examples.index            | App\Modules\Example\Controllers\ExamplesController@index               | web           |
| POST     | examples                  | examples.store            | App\Modules\Example\Controllers\ExamplesController@store               | web           |
| GET|HEAD | examples/create           | examples.create           | App\Modules\Example\Controllers\ExamplesController@create              | web           |
| DELETE   | examples/{id}             | examples.destroy          | App\Modules\Example\Controllers\ExamplesController@destroy             | web           |
| PATCH    | examples/{id}             | examples.update           | App\Modules\Example\Controllers\ExamplesController@update              | web           |
| GET|HEAD | examples/{id}             | examples.show             | App\Modules\Example\Controllers\ExamplesController@show                | web           |
| GET|HEAD | examples/{id}/edit        | examples.edit             | App\Modules\Example\Controllers\ExamplesController@edit                | web           |
+----------+---------------------------+---------------------------+------------------------------------------------------------------------+---------------+

Enter fullscreen mode Exit fullscreen mode

最后想说的

每个模块代表一个用例,但它们可以组合成一个域,例如,模块/CMS可以包含以下“子模块”:文章、标签、分类等等。演示实现完成后,我会发布相关更新。我还在开发演示器并添加测试,这些内容也会在下一个版本中进行介绍。


请在评论区告诉我你的想法。

欢迎您通过提交 pull request 来提出对代码库的修改建议。非常感谢您的贡献!

感谢您抽出时间。

此实现的灵感来源于nWidart/laravel-modulesArtem-Schander/L5Modular。

链接:


文章来源:https://dev.to/keljtanoski/modular-laravel-3dkf