Cette documentation vous guide à travers l'utilisation de la bibliothèque de base pour implémenter une architecture propre dans PHP. Nous explorerons la création de demandes d'application personnalisées et de cas d'utilisation, en accordant une attention particulière à la gestion des champs manquants et non autorisés.
Des exemples pratiques sont fournis à l'aide d'extraits de code pour présenter l'utilisation de la bibliothèque dans la création d'une application PHP modulaire et propre.
Assurez-vous que vous avez ce qui suit:
PHP installé sur votre machine (version 8.2.0 or higher ).Composer installé pour la gestion des dépendances. Pour installer la bibliothèque de base, exécutez la commande suivante dans votre répertoire de projet:
composer require ug-php/clean-architecture-core La demande sert d'objets d'entrée, encapsulant les données de votre contrôleur HTTP. Dans la bibliothèque de base, utilisez la classe UrichyCoreRequestRequest comme base pour la création d'objets de demande d'application personnalisés. Définissez les champs attendus à l'aide de la propriété 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 ();
}
}
}Gestion des champs non autorisés:
<?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"]
}Gestion des champs manquants:
<?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"]
}Lors de la demande de création avec succès.
<?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',
],
],
]*/ Le présentateur gère la logique de sortie de votre cas d'utilisation. Extendez UrichyCorePresenterPresenter et implément 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 ();
}
} La réponse résume les données renvoyées par les cas d'utilisation. Ils incluent les informations de statut, les messages et toutes les données pertinentes. Utiliser UrichyCoreResponseResponse pour créer des réponses de cas d'utilisation.
<?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 Les cas d'utilisation encapsulent la logique métier et orchestrent le flux de données entre les demandes, les entités et les présentateurs. Étendez la classe UrichyCoreUsecaseUsecase et implémentez UrichyCoreUsecaseUsecaseInterface avec la méthode execute .
@see example below
Lorsqu'une exception est lancée pendant le traitement, vous pouvez utiliser une méthode pour gérer les données d'exception.
Comment créer une exception?
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.phpAvec demande et présentateur
<?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 ' ]);
}
}Sans présentateur, mais avec demande.
<?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 ([]);
}
}Sans demande et présentateur
<?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