이 문서는 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 Validation Library 사용 <?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