L'organisation du code metier dans laravel 10

Découvrez une façon simple d'organiser son code dans laravel avec les services.

L'organisation du code metier dans laravel 10

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.