模块化 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
│ └──
└──
接口
主要接口是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);
}
实现了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);
}
}
另外两个接口分别是搜索接口和筛选接口。
SearchInterface定义了一个方法,当从数据库检索数据时需要搜索过滤器时,每个模块的特定存储库类可以实现此接口。
namespace App\Modules\Core\Interfaces;
interface SearchInterface
{
/**
* @param array $request
* @return mixed
*/
public function search(array $request);
}
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);
}
}
}
这还可以进一步抽象化,但我会在未来的版本中处理这个问题😄
此外,FilterInterface只定义了一个方法,如果需要按特定请求键进行过滤,则每个模块的每个 Filter 类都会实现此接口。
namespace App\Modules\Core\Interfaces;
interface FilterInterface
{
/**
* @param $value
* @return mixed
*/
public function handle($value);
}
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 . '%');
}
}
例外情况
Exceptions目录包含通用异常,其中包含一些预定义的$code和$message,当每个模块的自定义异常扩展通用异常时,这些预定义值可以被覆盖。
例如,在提供的模块示例中,定义了多个异常。
ExampleNotFoundException
namespace App\Modules\Example\Exceptions;
use App\Modules\Core\Exceptions\GeneralNotFoundException;
class ExampleNotFoundException extends GeneralNotFoundException
{
}
这扩展了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";
}
}
请求
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)
);
}
}
然后,其他每个抽象表单请求类都继承自这个抽象表单请求类。
创建表单请求
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();
}
现在,当抽象类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'
],
];
}
}
特征
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);
}
要了解核心结构的大致情况,请克隆此仓库或创建一个新的 Composer 项目。
git clone https://github.com/keljtanoski/modular-laravel.git
composer create-project keljtanoski/modular-laravel
示例模块结构
这是一个可直接使用的示例模块。该模块的主要目的是演示接口、存储库和服务之间的交互,它可以轻松复制,只需简单的查找和替换操作,即可快速搭建并运行新的模块。
目录概览
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
│ └──
└──
控制器
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;
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
}
示例控制器
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");
}
}
服务
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;
});
}
}
该模块中的ExampleService类结构如下:
namespace App\Modules\Example\Services;
class ExampleService
{
public $exampleRepository;
public function __construct(ExampleInterface $exampleRepository)
{
$this->exampleRepository = $exampleRepository;
}
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);
}
}
getAll()方法
/**
* @return mixed
* @throws ExampleIndexException
*/
public function getAll()
{
try {
return $this->exampleRepository->findAll();
} catch (Exception $exception) {
throw new ExampleIndexException($exception);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
要了解模块结构的大致情况,请克隆仓库或创建一个新的 Composer 项目。
git clone https://github.com/keljtanoski/modular-laravel.git
composer create-project keljtanoski/modular-laravel
路线列表
这只是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 |
+----------+---------------------------+---------------------------+------------------------------------------------------------------------+---------------+
最后想说的
每个模块代表一个用例,但它们可以组合成一个域,例如,模块/CMS可以包含以下“子模块”:文章、标签、分类等等。演示实现完成后,我会发布相关更新。我还在开发演示器并添加测试,这些内容也会在下一个版本中进行介绍。
请在评论区告诉我你的想法。
欢迎您通过提交 pull request 来提出对代码库的修改建议。非常感谢您的贡献!
感谢您抽出时间。
此实现的灵感来源于nWidart/laravel-modules和Artem-Schander/L5Modular。
链接:
- https://packagist.org/packages/keljtanoski/modular-laravel
- https://github.com/keljtanoski/modular-laravel
文章来源:https://dev.to/keljtanoski/modular-laravel-3dkf