L'organisation du code metier dans laravel 10
Découvrez une façon simple d'organiser son code dans laravel avec les services.
Une des problématiques courrantes dans mes applications développées avec laravel consiste à arbitrer lorsqu’un test doit renvoyer une erreure, ou une exception.
Dans certain cas un test doit intérrompre l'execution du programme ( donc lancer une exception ) et dans d'autre cas juste me permettre par exemple d'afficher un message d'information.
Exemple: l'abence de tarifs définis pour un magasin donné doit m'empecher d'éditer une facture, alors que du point de vu de la gestion des magasins je veux juste signifier à l'administrateur que les tarifs ne sont pas définis sur tel magasin.
Pour résoudre cette problématique, j'utilise un pattern Service que je vais vous décrire ici.
Dans app/Services nous créons une classe BaseService
<?php
namespace App\Services;
use Exception;
class BaseService
{
public bool $throw;
const THROW = true;
public function __construct(bool $throw = false)
{
$this->throw = $throw;
}
public function falseOrThrow(Exception $e)
{
if ($this->throw) {
throw $e;
}
return false;
}
}
Pour chaque Model qui implique du fonctionel j’ai des services spécifiques, ici mon projet a un model Place et un modèle Report.
dans un sous dossier App\Services\Rules, j’ai des services spécifiques dédiés à la validation de données, dans cet exemple j’ai :
App\Services\Rules\PlaceRulesService:
<?php
namespace App\Services\Rules;
use App\Models\Place;
use App\Enums\PriceType;
use Illuminate\Support\Arr;
use App\Services\BaseService;
use Exception;
class PlaceRulesService extends BaseService
{
public function hasEachTypeOfPriceDefined(Place $place): bool
{
$types = array_column(PriceType::cases(), 'value');
$placeTypes = $place->prices()->pluck('type')->toArray();
if (Arr::sort($types) !== Arr::sort($placeTypes)) {
return $this->falseOrThrow(new Exception('All types of price are defined fr that place'));
}
return true;
}
}
J’ai en suite une sorte de Helper que j’appelle “service”:
<?php
namespace App\Services;
use App\Services\Rules\PatientRulesService;
use App\Services\Rules\PlaceRulesService;
use App\Services\Rules\ReportRulesService;
use App\Services\Rules\SpecialistRulesService;
use Illuminate\Support\Facades\App;
class Service
{
public static function reportRules($throw = false): ReportRulesService
{
return App::make(ReportRulesService::class, ['throw' => $throw]);
}
public static function placeRules($throw = false): PlaceRulesService
{
return App::make(PlaceRulesService::class, ['throw' => $throw]);
}
}
Dans mon code, si je doit appeler hasEachTypeOfPriceDefined sur une place, si l’erreur doit être bloquante et générer une exception je fais :
Service::placeRules(BaseService::THROW)->hasEachTypeOfPriceDefined($place);
Si l’appel à la méthode ne doit pas générer d’erreur je fais :
<p>Les tarifs sont bien définis : {{ Service::placeRules()->hasEachTypeOfPriceDefined($place) ? 'oui' : 'non' }}</p>
En utilisant Pest2 du Brillant Nuno Maduro, on peux s'assurer que tel bloc fonctionel renvoi une exception ou juste false.
<?php
use App\Models\Place;
use App\Services\Service;
use App\Services\BaseService;
it('ensures hasEachTypeOfPriceDefined throw exception', function () {
$place = Place::factory()->create();
Service::placeRules(BaseService::THROW)->hasEachTypeOfPriceDefined($place);
})->throws(Exception::class, 'Tous les types de prix ne sont pas définis pour ce lieu');
it('ensures hasEachTypeOfPriceDefined return false', function () {
$place = Place::factory()->create();
expect(Service::placeRules()->hasEachTypeOfPriceDefined($place))->tobe(false);
});
Les services me permettent également de régler d'autres problématiques d'organisation du code dans Laravel 10, nous y reviendrons.
A suivre.