clean architecture core php
v1.2.1
该文档可以指导您通过核心库在PHP中实现干净体系结构的利用。我们将探讨自定义应用程序请求和用例的创建,特别注意处理丢失和未经授权的字段。
使用代码片段提供了实践示例,以展示图书馆在构建模块化和干净的PHP应用程序中的用法。
确保您有以下内容:
PHP安装在计算机上(版本8.2.0 or higher )。Composer 。 要安装核心库,请在项目目录中运行以下命令:
composer require ug-php/clean-architecture-core请求用作输入对象,将HTTP控制器的数据封装。在核心库中,使用UrichyCoreRequestRequest类作为创建自定义应用程序请求对象的基础。使用requestPossibleFields属性定义预期字段。
<?php
declare (strict_types= 1 );
use Urichy Core Request Request ;
use Urichy Core Request RequestInterface ;
use Assert Assert ;
final class PatientRecordRequest extends Request
{
protected static array $ requestPossibleFields = [
' patient_name ' => true , // required parameter
' old ' => true , // required parameter
' medical_history ' => [
' allergies ' => false , // optional parameter
' current_medications ' => true , // required nested parameter
' past_surgeries ' => [
' surgery_name ' => true , // required nested parameter
' surgery_date ' => true , // required nested parameter
],
],
];
protected static function applyConstraintsOnRequestFields ( array $ requestData ): void
{
Assert:: that ( $ requestData [ ' patient_name ' ], ' [patient_name] must not be an empty string. ' )-> notEmpty ()-> string ();
Assert:: that ( $ requestData [ ' old ' ], ' [old] must be an integer. ' )-> integer ()-> greaterThan ( 0 );
Assert:: that ( $ requestData [ ' medical_history ' ][ ' current_medications ' ], ' [current_medications] must not be an empty string. ' )-> notEmpty ()-> string ();
Assert:: that ( $ requestData [ ' medical_history ' ][ ' past_surgeries ' ][ ' surgery_name ' ], ' [surgery_name] must not be an empty string. ' )-> notEmpty ()-> string ();
Assert:: that ( $ requestData [ ' medical_history ' ][ ' past_surgeries ' ][ ' surgery_date ' ], ' [surgery_date] must be a valid date. ' )-> date ();
// Optional field constraint
if ( isset ( $ requestData [ ' medical_history ' ][ ' allergies ' ])) {
Assert:: that ( $ requestData [ ' medical_history ' ][ ' allergies ' ], ' [allergies] must be a string. ' )-> string ();
}
}
}处理未经授权的领域:
<?php
declare (strict_types= 1 );
try {
PatientRecordRequest:: createFromPayload ([
' patient_name ' => ' Jane Doe ' ,
' old ' => 45 ,
' medical_history ' => [
' current_medications ' => ' aspirin ' ,
' past_surgeries ' => [
' surgery_name ' => ' Appendectomy ' ,
' surgery_date ' => ' 2022-01-01 ' ,
],
' extra_field ' => ' unexpected ' ,
],
]);
} catch ( BadRequestContentException $ exception ) {
// Handle unauthorized fields
dd ( $ exception -> getErrors ()); // ["medical_history.extra_field"]
}处理缺失的字段:
<?php
declare (strict_types= 1 );
try {
PatientRecordRequest:: createFromPayload ([
' patient_name ' => ' Jane Doe ' ,
' medical_history ' => [
' current_medications ' => ' aspirin ' ,
' past_surgeries ' => [
' surgery_name ' => ' Appendectomy ' ,
],
],
]);
} catch ( BadRequestContentException $ exception ) {
// Handle missing fields
dd ( $ exception -> getErrors ()); // ["old" => "required", "medical_history.past_surgeries.surgery_date" => "required"]
}当请求成功创建时。
<?php
declare (strict_types= 1 );
$ request = PatientRecordRequest:: createFromPayload ([
' patient_name ' => ' Jane Doe ' ,
' old ' => 45 ,
' medical_history ' => [
' current_medications ' => ' aspirin ' ,
' past_surgeries ' => [
' surgery_name ' => ' Appendectomy ' ,
' surgery_date ' => ' 2022-01-01 ' ,
],
],
]);
dd ( $ request -> getRequestId ()); // 6d326314-f527-483c-80df-7c157acdb95b
dd ([
' patient_name ' => $ request -> get ( ' patient_name ' ),
' current_medications ' => $ request -> get ( ' medical_history.current_medications ' ),
' unknown ' => $ request -> get ( ' unknown ' , ' default_value ' ),
]); // ['patient_name' => 'Jane Doe', 'current_medications' => 'aspirin', 'unknown' => 'default_value']
dd ( $ request -> toArray ());
/*
[
'patient_name' => 'Jane Doe',
'old' => 45,
'medical_history' => [
'current_medications' => 'aspirin',
'past_surgeries' => [
'surgery_name' => 'Appendectomy',
'surgery_date' => '2022-01-01',
],
],
]*/主持人处理用例的输出逻辑。扩展UrichyCorePresenterPresenter ,并实现UrichyCorePresenterPresenterInterface 。
<?php
declare (strict_types= 1 );
use Urichy Core Presenter Presenter ;
use Urichy Core Presenter PresenterInterface ;
use Urichy Core Response ResponseInterface ;
final class ArrayResponsePresenter extends Presenter
{
public function getResponse (): array
{
return $ this -> response -> output ();
}
}响应封装了用例返回的数据。它们包括状态信息,消息和任何相关数据。使用UrichyCoreResponseResponse来创建用例响应。
<?php
declare (strict_types= 1 );
use Urichy Core Response Response ;
// success response
$ response = Response:: create (
success: true ,
statusCode: StatusCode:: OK -> value ,
message: ' success.response ' ,
data: [
' user_id ' => ' 6d326314-f527-483c-80df-7c157acdb95b ' ,
]
)
// or failed response
$ response = Response:: create (
success: false ,
statusCode: StatusCode:: NOT_FOUND -> value ,
message: ' failed.response ' ,
data: [
' field ' => ' value ' ,
]
)
dd ( $ response -> isSuccess ()); // true or false
dd ( $ response -> getStatusCode ()); // 200 or 404
dd ( $ response -> getMessage ()); // 'success.response' or 'failed.response'
dd ( $ response -> getData ()); // ['field' => 'value'] or ['user_id' => '6d326314-f527-483c-80df-7c157acdb95b']
dd ( $ response -> get ( ' field ' )); // 'value'
dd ( $ response -> get ( ' unknown_field ' )); // null用例封装业务逻辑,并在请求,实体和演示者之间进行数据流程。扩展UrichyCoreUsecaseUsecase类,并用execute方法实现UrichyCoreUsecaseUsecaseInterface 。
@see example below
当处理过程中抛出异常时,您可以使用一些方法来处理异常数据。
如何创建例外?
UrichyCoreExceptionException异常类 <?php
declare (strict_types= 1 );
use Urichy Core Exception Exception ;
final class BadRequestContentException extends Exception
{
}
final class UserNotFoundException extends Exception
{
} <?php
declare (strict_types= 1 );
use Urichy Core Exception Exception ;
use Urichy Core Exception BadRequestContentException ;
use Urichy Core Exception UserNotFoundException ;
try {
//...
throw new BadRequestContentException ([
' message ' => ' bad.request.content ' ,
' details ' => [
' email ' => [
' [email] field is required. ' ,
' [email] must be a valid email. ' ,
]
] // array with error contexts
]);
// or
throw new UserNotFoundException ([
' message ' => ' user.not.found ' ,
' details ' => [
' error ' => ' User with [ulrich] username not found. '
] // array with error contexts
]);
} catch ( ExceptionInterface $ exception ) {
// for exception, some method are available
dd ( $ exception -> getErrors ()); // print details
[
' details ' => [
' email ' => [
' [email] field is required. ' ,
' [email] must be a valid email. ' ,
]
],
]
// or
[
' details ' => [
' error ' => ' User with [ulrich] username not found. ' ,
],
]
dd ( $ exception -> getDetails ()); // print error details
[
' email ' => [
' [email] field is required. ' ,
' [email] must be a valid email. ' ,
]
]
// or
[
' error ' => ' User with [ulrich] username not found. ' ,
],
dd ( $ exception -> getMessage ()) // 'error.message'
dd ( $ exception -> getDetailsMessage ()) // 'User with [ulrich] username not found.', only if 'error' key is defined in details.
dd ( $ exception -> getErrorsForLog ()) // print error with more context
[
' message ' => $ this -> getMessage (),
' code ' => $ this -> getCode (),
' errors ' => $ this -> errors ,
' file ' => $ this -> getFile (),
' line ' => $ this -> getLine (),
' previous ' => $ this -> getPrevious (),
' trace_as_array ' => $ this -> getTrace (),
' trace_as_string ' => $ this -> getTraceAsString (),
]
dd ( $ exception -> format ());
[
' status ' => ' success ' or ' error ' ,
' error_code ' => 400 ,
' message ' => ' throw.error ' ,
' details ' => [
' email ' => [
' [email] field is required. ' ,
' [email] must be a valid email. ' ,
],
' lastname ' => [
' [lastname] field is required. ' ,
]
],
]
} ├── src
│ ├── Controller
│ │ └── BookController.php
│ ├── Request
│ │ └── BookRecordRequest.php
│ ├── Presenter
│ │ └── JsonResponsePresenter.php
│ ├── UseCase
│ │ └── RegisterBookUsecase.php
│ └── Response
│ └── Response.php
├── public
│ └── index.php
└── composer.json
public/index.php <?php
declare (strict_types= 1 );
require ' ../vendor/autoload.php ' ;
use App Controller BookController ;
use Symfony Component HttpFoundation Request ;
$ request = Request:: createFromGlobals ();
$ controller = new BookController ();
$ response = $ controller -> registerBook ( $ request );
$ response -> send ();src/Controller/BookController.php <?php
declare (strict_types= 1 );
namespace App Controller ;
use App Request BookRecordRequest ;
use App Presenter JsonResponsePresenter ;
use App UseCase RegisterBookUsecase ;
use Symfony Component HttpFoundation Request as SymfonyRequest ;
use Symfony Component HttpFoundation JsonResponse ;
final class BookController
{
public function registerBook ( SymfonyRequest $ request ): JsonResponse
{
$ bookRequest = BookRecordRequest:: createFromPayload ([
' title ' => $ request -> get ( ' title ' ),
' publication ' => [
' date ' => $ request -> get ( ' published_date ' ),
' publisher ' => $ request -> get ( ' publisher ' ),
],
' isbn ' => $ request -> get ( ' isbn ' ),
]);
// you can also use $request->toArray() (in createFromPayload method) to get request payload if POST request
$ presenter = new JsonResponsePresenter ();
$ useCase = new RegisterBookUsecase ();
$ useCase
-> withRequest ( $ bookRequest )
-> withPresenter ( $ presenter )
-> execute ();
return $ presenter -> getResponse ();
}
}src/Request/BookRecordRequest.php beberlei/assert验证库 <?php
declare (strict_types= 1 );
namespace App Request ;
use Urichy Core Request Request ;
use Urichy Core Request RequestInterface ;
use Assert Assert ;
// interface is optional. You can directly use the implementation
interface BookRecordRequestInterface extends RequestInterface {}
final class BookRecordRequest extends Request implements BookRecordRequestInterface
{
protected static array $ requestPossibleFields = [
' title ' => true , // required parameters
' publication ' => [
' date ' => true ,
' publisher ' => false , // optional parameters
],
' isbn ' => true ,
];
/**
* @param array<string, mixed> $requestData
* @return void
*/
protected static function applyConstraintsOnRequestFields ( array $ requestData ): void
{
Assert:: that ( $ requestData [ ' title ' ], ' [title] must not be an empty string. ' )-> notEmpty ()-> string ();
Assert:: that ( $ requestData [ ' publication ' ][ ' date ' ], ' [date] must be a valid date. ' )-> date ();
Assert:: that ( $ requestData [ ' isbn ' ], ' [isbn] must not be an empty string. ' )-> notEmpty ()-> string ();
if ( isset ( $ requestData [ ' publication ' ][ ' publisher ' ])) {
Assert:: that ( $ requestData [ ' publication ' ][ ' publisher ' ], ' [publisher] must be a string. ' )-> string ();
}
}
}Symfony Validator库 <?php
declare (strict_types= 1 );
namespace App Request ;
use Urichy Core Request Request ;
use Urichy Core Request RequestInterface ;
use Symfony Component Validator Validation ;
use Symfony Component Validator Constraints as SymfonyAssert ;
use Symfony Component Validator ConstraintViolationListInterface ;
// interface is optional. You can directly use the implementation
interface BookRecordRequestInterface extends RequestInterface {}
final class BookRecordRequest extends Request implements BookRecordRequestInterface
{
protected static array $ requestPossibleFields = [
' title ' => true ,
' publication ' => [
' date ' => true ,
' publisher ' => false ,
],
' isbn ' => true ,
];
/**
* @param array<string, mixed> $requestData
* @return void
*/
protected static function applyConstraintsOnRequestFields ( array $ requestData ): void
{
$ validator = Validation:: createValidator ();
$ constraints = [
' title ' => [
new SymfonyAssert NotBlank (message: ' [title] cannot be blank ' ),
new SymfonyAssert Type (type: ' string ' , message: ' [title] must be a string ' ),
],
' publication ' => new SymfonyAssert Collection ([
' date ' => [
new SymfonyAssert NotBlank (message: ' [date] cannot be blank ' ),
new SymfonyAssert Date (message: ' [date] must be a valid date ' ),
]
]),
' isbn ' => [
new SymfonyAssert NotBlank (message: ' [isbn] cannot be blank ' ),
new SymfonyAssert Type (type: ' string ' , message: ' [isbn] must be a string ' ),
],
];
if ( isset ( $ requestData [ ' publication ' ][ ' publisher ' ])) {
$ constraints [ ' publication ' ][ ' publisher ' ] = [
new SymfonyAssert Type (type: ' string ' , message: ' [publisher] must be a string ' ),
];
}
$ violations = $ validator -> validate ( $ requestData , new SymfonyAssert Collection ( $ constraints ));
self :: throwViolationsWhenErrors ( $ violations );
}
private static function throwViolationsWhenErrors ( ConstraintViolationListInterface $ violations ): void
{
$ errors = [];
foreach ( $ violations as $ violation ) {
$ propertyPath = $ violation -> getPropertyPath ();
$ errors [ $ propertyPath ][] = $ violation -> getMessage ();
}
if (! empty ( $ errors )) {
$ errors [ ' message ' ] = ' invalid.request.field ' ;
throw new BadRequestContentException ( $ errors );
}
}
}src/Presenter/JsonResponsePresenter.php <?php
declare (strict_types= 1 );
namespace App Presenter ;
use Urichy Core Presenter Presenter ;
use Urichy Core Presenter PresenterInterface ;
use Symfony Component HttpFoundation JsonResponse ;
final class JsonResponsePresenter extends Presenter implements PresenterInterface
{
public function getResponse (): JsonResponse
{
$ responseData = $ this -> response -> output ();
return new JsonResponse ( $ responseData , $ responseData [ ' code ' ]);
}
}src/Presenter/HtmlResponsePresenter.php <?php
declare (strict_types= 1 );
namespace App Presenter ;
use Urichy Core Presenter Presenter ;
use Urichy Core Presenter PresenterInterface ;
use Symfony Component HttpFoundation Response ;
final class HtmlResponsePresenter extends Presenter implements PresenterInterface
{
public function getResponse (): Response
{
$ responseData = $ this -> response -> output ();
$ htmlContent = " <html><body><h1> { $ responseData [ ' message ' ]} </h1><p> " . json_encode ( $ responseData [ ' data ' ]) . " </p></body></html> " ;
return new Response ( $ htmlContent , $ responseData [ ' code ' ]);
}
}src/UseCase/RegisterBookUsecase.php <?php
declare (strict_types= 1 );
namespace App UseCase ;
use Urichy Core Usecase Usecase ;
use Urichy Core Usecase UsecaseInterface ;
use Urichy Core Response Response ;
use Urichy Core Response StatusCode ;
interface RegisterBookUsecaseInterface extends UsecaseInterface {}
final class RegisterBookUsecase extends Usecase implements RegisterBookUsecaseInterface
{
public function __construct (
// inject your dependencies here (always use dependencie interface, not implementation)
private BookRepositoryInterface $ bookRepository
) {}
public function execute (): void
{
$ requestData = $ this -> getRequestData ();
$ requestId = $ this -> getRequestId ();
$ book = [
' title ' => $ this -> getField ( ' title ' ),
' author ' => $ this -> getField ( ' publication.publisher ' ),
' publication_date ' => $ this -> getField ( ' publication.date ' ),
' isbn ' => $ this -> getField ( ' isbn ' ),
];
// process your business logic here
try {
$ this -> bookRepository -> save (Book:: from ( $ book ))
} catch ( PersistenceException $ e ) {
// handle persistence exception here or log it or send failed response.
}
$ this -> presentResponse (Response:: create (
success: true ,
statusCode: StatusCode:: OK -> value ,
message: ' book.registered.successfully. ' ,
data: $ book
));
}
}src/Response/Response.php <?php
declare (strict_types= 1 );
namespace App Response ;
use Urichy Core Response Response as LibResponse ;
use Urichy Core Response StatusCode ;
abstract class Response extends LibResponse
{
public static function createSuccessResponse ( array $ data , StatusCode $ statusCode , ? string $ message = null ): self
{
return new self ( true , $ statusCode -> value , $ message , $ data );
}
public static function createFailedResponse ( array $ errors = [], StatusCode $ statusCode , ? string $ message = null ): self
{
return new self ( false , $ statusCode -> value , $ message , $ errors );
}
} ├── src
│ ├── Controller
│ │ └── BookController.php
│ ├── Request
│ │ └── BookRecordRequest.php
│ ├── Presenter
│ │ └── JsonResponsePresenter.php
| | └── HtmlResponsePresenter.php
│ ├── UseCase
│ │ └── RegisterBookUsecase.php
│ └── Response
│ └── Response.php
├── public
│ └── index.php
├── config
│ └── services.yaml
└── composer.json
src/Controller/BookController.php <?php
declare (strict_types= 1 );
namespace App Controller ;
use App Request BookRecordRequest ;
use App Presenter JsonResponsePresenter ;
use App Presenter HtmlResponsePresenter ;
use App UseCase RegisterBookUsecase ;
use Symfony Component HttpFoundation Request as SymfonyRequest ;
use Symfony Component HttpFoundation JsonResponse ;
use Symfony Bundle FrameworkBundle Controller AbstractController ;
use Symfony Component Routing Annotation Route ;
#[Route( ' /register-book ' , name: ' register_book ' , methods: ' POST ' )]
final class BookController extends AbstractController
{
public function __construct (
private readonly RegisterBookUsecase $ registerBookUsecase
) {}
public function __invoke ( SymfonyRequest $ request ): JsonResponse
{
try {
$ bookRequest = BookRecordRequest:: createFromPayload ([
' title ' => $ request -> get ( ' title ' ),
' author ' => $ request -> get ( ' author ' ),
' publication ' => [
' published_date ' => $ request -> get ( ' published_date ' ),
' publisher ' => $ request -> get ( ' publisher ' ),
],
' isbn ' => $ request -> get ( ' isbn ' ),
]);
$ presenter = $ this - getPresenterAccordingToRequestContentType ( $ request -> getContentType ());
$ this -> registerBookUsecase
-> withRequest ( $ bookRequest )
-> withPresenter ( $ presenter )
-> execute ();
$ response = $ presenter -> getResponse ()-> output ();
} catch ( Exception $ exception ) {
return $ this -> json ( $ exception -> format (), $ exception -> getCode ());
}
return $ this -> json ( $ response , $ response [ ' code ' ]);
}
// you can instanciate presenter according to the request context
private function getPresenterAccordingToRequestContentType ( string $ contentType ): PresenterInterface
{
switch ( $ contentType ) {
case ' text/html ' :
return new HtmlResponsePresenter ();
default :
break ;
}
return new JsonResponsePresenter ();
}
} ├── app
│ ├── Http
│ │ └── Controllers
│ │ └── BookController.php
│ ├── Requests
│ │ └── BookRecordRequest.php
│ ├── Presenters
│ │ └── JsonResponsePresenter.php
│ ├── UseCases
│ │ └── RegisterBookUsecase.php
│ └── Responses
│ └── Response.php
├── public
│ └── index.php
└── composer.json
app/Http/Controllers/BookController.php带有请求和主持人
<?php
declare (strict_types= 1 );
namespace App Http Controllers ;
use App Requests BookRecordRequest ;
use App Presenters JsonResponsePresenter ;
use App Presenters HtmlResponsePresenter ;
use App UseCases RegisterBookUsecase ;
use Illuminate Http Request as LaravelRequest ;
use Illuminate Http JsonResponse ;
final class BookController extends Controller
{
public function __construct (
private readonly RegisterBookUsecase $ registerBookUsecase
) {}
public function __invoke ( LaravelRequest $ request ): JsonResponse
{
try {
$ bookRequest = BookRecordRequest:: createFromPayload ([
' title ' => $ request -> input ( ' title ' ),
' author ' => $ request -> input ( ' author ' ),
' publication ' => [
' published_date ' => $ request -> input ( ' published_date ' ),
' publisher ' => $ request -> input ( ' publisher ' ),
],
' isbn ' => $ request -> input ( ' isbn ' ),
]);
$ jsonPresenter = new JsonResponsePresenter ();
$ this
-> registerBookUsecase
-> withRequest ( $ bookRequest )
-> withPresenter ( $ jsonPresenter )
-> execute ();
$ response = $ jsonPresenter -> getResponse ()-> output ();
} catch ( Exception $ exception ) {
return response ()-> json ( $ exception -> format (), $ exception -> getCode ());
}
return response ()-> json ( $ response , $ response [ ' code ' ]);
}
}没有演示者,但请要求。
<?php
declare (strict_types= 1 );
namespace App Http Controllers ;
use App Requests BookRecordRequest ;
use App UseCases RegisterBookUsecase ;
use Illuminate Http Request as LaravelRequest ;
use Illuminate Http JsonResponse ;
final class BookController extends Controller
{
public function __construct (
private readonly RegisterBookUsecase $ registerBookUsecase
) {}
public function __invoke ( LaravelRequest $ request ): JsonResponse
{
try {
$ bookRequest = BookRecordRequest:: createFromPayload ([
' title ' => $ request -> input ( ' title ' ),
' author ' => $ request -> input ( ' author ' ),
' publication ' => [
' published_date ' => $ request -> input ( ' published_date ' ),
' publisher ' => $ request -> input ( ' publisher ' ),
],
' isbn ' => $ request -> input ( ' isbn ' ),
]);
$ this
-> registerBookUsecase
-> withRequest ( $ bookRequest )
-> execute ();
} catch ( Exception $ exception ) {
return response ()-> json ( $ exception -> format (), $ exception -> getCode ());
}
return response ()-> json ([]);
}
}没有请求和主持人
<?php
declare (strict_types= 1 );
namespace App Http Controllers ;
use App Requests BookRecordRequest ;
use App UseCases RegisterBookUsecase ;
use Illuminate Http Request as LaravelRequest ;
use Illuminate Http JsonResponse ;
final class BookController extends Controller
{
public function __construct (
private readonly RegisterBookUsecase $ registerBookUsecase
) {}
public function __invoke (): JsonResponse
{
try {
$ this
-> registerBookUsecase
-> execute ();
} catch ( Exception $ exception ) {
return response ()-> json ( $ exception -> format (), $ exception -> getCode ());
}
return response ()-> json ([]);
}
}$ make tests