web-dev-qa-db-fra.com

MVC (Laravel) où ajouter de la logique

Disons que chaque fois que je fais une opération CRUD ou modifie une relation d'une manière spécifique, je veux aussi faire autre chose. Par exemple, chaque fois que quelqu'un publie un article, je souhaite également enregistrer un élément dans une table pour analyse. Peut-être pas le meilleur exemple, mais en général il y a beaucoup de cette fonctionnalité "groupée".

Normalement, je vois ce type de logique dans les contrôleurs. Tout va bien jusqu'à ce que vous souhaitiez reproduire cette fonctionnalité dans de nombreux endroits. Lorsque vous commencez à vous lancer dans les partiels, à créer une API et à générer un contenu factice, il devient difficile de garder les éléments au sec.

Les moyens que j'ai observés pour gérer cela sont les événements, les référentiels, les bibliothèques et les ajouts aux modèles. Voici ma compréhension de chacun:

Services: C'est ici que la plupart des gens mettront probablement ce code. Mon problème principal avec les services est qu’il est parfois difficile de trouver des fonctionnalités spécifiques et que j’ai le sentiment qu’ils sont oubliés lorsque les gens se concentrent sur l’utilisation d’Eloquent. Comment saurais-je que j'ai besoin d'appeler une méthode publishPost() dans une bibliothèque alors que je ne peux que faire $post->is_published = 1?

La seule condition dans laquelle je vois que cela fonctionne bien est que vous utilisez UNIQUEMENT des services (et, dans l’idéal, rend Éloquent inaccessible d’une manière ou d’une autre par les contrôleurs dans leur ensemble).

En fin de compte, il semble que cela ne ferait que créer un tas de fichiers superflus si vos demandes suivent généralement la structure de votre modèle.

Repositories: D'après ce que j'ai compris, il s'agit en gros d'un service, mais il existe une interface vous permettant de basculer entre les ORM, ce dont je n'ai pas besoin.

Events: Je considère cela comme le système le plus élégant dans un sens, car vous savez que vos événements de modèle seront toujours appelés par des méthodes Eloquent. Vous pourrez ainsi écrire vos contrôleurs comme vous le feriez normalement. Je peux toutefois voir que ces problèmes deviennent désordonnés et si quelqu'un a des exemples de grands projets utilisant des événements pour un couplage critique, j'aimerais le voir.

Models: Traditionnellement, j'aurais des classes qui exécutent CRUD et gèrent également les couplages critiques. Cela a en fait facilité les choses, car vous connaissiez toutes les fonctionnalités de CRUD +, tout ce qui devait être fait avec était là.

Simple, mais dans l'architecture MVC, ce n'est pas normalement ce que je vois faire. Dans un sens, je préfère cela aux services, car il est un peu plus facile à trouver et il y a moins de fichiers à garder en mémoire. Cela peut cependant être un peu désorganisé. J'aimerais entendre les inconvénients de cette méthode et expliquer pourquoi la plupart des gens ne semblent pas le faire.

Quels sont les avantages/inconvénients de chaque méthode? Est-ce que je manque quelque chose?

115
Sabrina Leggett

Je pense que tous les modèles/architectures que vous présentez sont très utiles tant que vous suivez les principes SOLIDE .

Pour le où ajouter la logique Je pense qu'il est important de se référer au principe de responsabilité unique . De plus, ma réponse considère que vous travaillez sur un projet de taille moyenne/grande. S'il s'agit d'un projet lancez quelque chose sur une page, oubliez cette réponse et ajoutez-la aux contrôleurs ou aux modèles.

La réponse courte est: Où cela vous semble judicieux (avec services) .

La réponse longue:

Contrôleurs : Quelle est la responsabilité des contrôleurs? Bien sûr, vous pouvez mettre toute votre logique dans un contrôleur, mais est-ce la responsabilité du contrôleur? Je ne pense pas.

Pour moi, le contrôleur doit recevoir une demande et renvoyer des données et ce n’est pas le lieu de mettre des validations, d’appeler des méthodes, etc.

Modèles : Est-ce un bon endroit pour ajouter une logique comme envoyer un email de bienvenue lorsqu'un utilisateur enregistre ou met à jour le nombre de votes d'un message? Et si vous avez besoin d'envoyer le même email depuis un autre endroit dans votre code? Créez-vous une méthode statique? Que se passe-t-il si les courriels ont besoin d'informations provenant d'un autre modèle?

Je pense que le modèle devrait représenter une entité. Avec Laravel, j’utilise la classe de modèle uniquement pour ajouter des éléments tels que fillable, guarded, table et les relations (c’est parce que j’utilise le modèle de référentiel, sinon le modèle également les méthodes save, update, find, etc.).

Référentiels (modèle de référentiel) : Au début, j'étais très confus. Et, comme vous, je me suis dit "bon, j’utilise MySQL et c’est tout.".

Cependant, j'ai équilibré les avantages et les inconvénients de l'utilisation du modèle de référentiel et maintenant je l'utilise. Je pense que maintenant, à ce moment précis, je n’aurai besoin que d’utiliser MySQL. Mais, si dans trois ans, je dois passer à quelque chose comme MongoDB, le travail est en grande partie terminé. Tout cela au détriment d'une interface supplémentaire et d'une $app->bind(«interface», «repository»).

Evénements ( Observer Pattern ): Les événements sont utiles pour tout ce qui peut être lancé à n'importe quelle classe à n'importe quel moment. Pensez, par exemple, à l'envoi de notifications à un utilisateur. Lorsque vous avez besoin, vous déclenchez l'événement pour envoyer une notification à n'importe quelle classe de votre application. Ensuite, vous pouvez avoir une classe comme UserNotificationEvents qui gère tous vos événements déclenchés pour les notifications utilisateur.

Services : Jusqu'à présent, vous avez le choix d'ajouter une logique aux contrôleurs ou aux modèles. Pour moi, il est logique d'ajouter la logique au sein de Services . Regardons les choses en face, Services est un nom de fantaisie pour les classes. Et vous pouvez avoir autant de cours que vous le souhaitez au sein de votre application.

Prenons cet exemple: Il y a peu de temps, j'ai développé quelque chose comme Google Forms. J'ai commencé avec CustomFormService et finalement CustomFormService, CustomFormRender, CustomFieldService, CustomFieldRender, CustomAnswerService et CustomAnswerRender. Pourquoi? Parce que cela me semblait logique. Si vous travaillez en équipe, vous devez définir votre logique lorsque cela est logique pour l'équipe.

L'avantage d'utiliser Services vs Contrôleurs/Modèles est que vous n'êtes pas contraint par un seul contrôleur ou un seul modèle. Vous pouvez créer autant de services que nécessaire en fonction de la conception et des besoins de votre application. Ajoutez à cela l'avantage d'appeler un service dans n'importe quelle classe de votre application.

Cela va longtemps, mais je voudrais vous montrer comment j'ai structuré mon application:

app/
    controllers/
    MyCompany/
        Composers/
        Exceptions/
        Models/
        Observers/
        Sanitizers/
        ServiceProviders/
        Services/
        Validators/
    views
    (...)

J'utilise chaque dossier pour une fonction spécifique. Par exemple, le répertoire Validators contient une classe BaseValidator chargée du traitement de la validation, basée sur le $rules et $messages de validateurs spécifiques (généralement un pour chaque modèle). Je pourrais aussi facilement mettre ce code dans un service, mais il est logique pour moi d'avoir un dossier spécifique pour cela même s'il n'est utilisé que dans le service (pour le moment).

Je vous recommande de lire les articles suivants, car ils pourraient vous expliquer un peu mieux les choses:

Breaking the Mould de Dayle Rees (auteur de CodeBright): C’est là que j’ai tout mis ensemble, même si j’ai changé quelques petites choses pour répondre à mes besoins.

Découplage de votre code dans Laravel en utilisant des référentiels et des services de Chris Goosey: Ce message explique bien ce qu'est un service et le modèle de référentiel et comment ils s'harmonisent.

Les Laracasts ont également les référentiels simplifiés et responsabilité unique , qui sont de bonnes ressources avec des exemples pratiques (même si vous devez payer).

142
Luís Cruz

Je voulais poster une réponse à ma propre question. Je pourrais en parler pendant des jours, mais je vais essayer de publier rapidement cette information pour être sûre de la comprendre.

J'ai fini par utiliser la structure existante fournie par Laravel, ce qui signifie que j'ai conservé mes fichiers principalement en tant que modèle, vue et contrôleur. J'ai également un dossier de bibliothèques pour les composants réutilisables qui ne sont pas vraiment des modèles .

I DID N'ENROULE PAS MES MODÈLES DANS LES SERVICES/BIBLIOTHÈQUES . Toutes les raisons fournies ne m'ont pas convaincu à 100% Bien que je puisse me tromper, d’après ce que je peux voir, ils ne font que générer des tonnes de fichiers presque vides que je dois créer et basculer entre les modèles, tout en réduisant réellement les avantages d’éloquent ( particulièrement en ce qui concerne les modèles de RÉCUPÉRATION, par exemple en utilisant la pagination, les portées, etc.).

Je mets la logique métier DANS LES MODÈLES et accède directement à l'éloquent à partir de mes contrôleurs. J'utilise plusieurs approches pour m'assurer que la logique métier ne soit pas ignorée:

  • Accesseurs et mutateurs: Laravel a d'excellents accesseurs et mutateurs. Si je veux effectuer une action chaque fois qu'une publication est déplacée de brouillon à publié, je peux appeler cela en créant la fonction setIsPublishedAttribute et en incluant la logique correspondante
  • Remplacement de création/mise à jour, etc.: Vous pouvez toujours remplacer les méthodes Eloquent dans vos modèles pour inclure des fonctionnalités personnalisées. De cette façon, vous pouvez appeler une fonctionnalité pour n’importe quelle opération CRUD. Edit: Je pense qu’il ya un bug avec la redéfinition de la création dans les nouvelles Laravel versions (donc j’utilise les événements maintenant enregistrés au démarrage)
  • Validation: J'accroche ma validation de la même manière, par exemple, je vais exécuter la validation en ignorant les fonctions CRUD et également les accesseurs/mutateurs si nécessaire. Voir Esensi ou dwightwatson/validating pour plus d'informations.
  • Méthodes magiques: J'utilise les méthodes __get et __set de mes modèles pour y ajouter des fonctionnalités, le cas échéant.
  • Extension d'éloquent: Si vous souhaitez effectuer une action sur toutes les mises à jour/créer, vous pouvez même étendre éloquent et l'appliquer à plusieurs modèles.
  • Evénements: Il s’agit également d’un lieu simple et généralement convenu. À mon avis, le plus gros inconvénient des événements est qu'il est difficile de retracer les exceptions (ce n'est peut-être pas le nouveau cas avec le nouveau système d'événements de Laravel). J'aime également grouper mes événements en fonction de ce qu'ils font au lieu de quand ils sont appelés ... Par exemple, si vous avez un abonné MailSender qui écoute les événements qui envoient du courrier.
  • Ajout d'événements Pivot/BelongsToMany: L'un des problèmes avec lequel j'ai le plus longtemps lutté était de savoir comment attacher un comportement à la modification des relations d'appartements-à-beaucoup. Par exemple, effectuer une action chaque fois qu'un utilisateur rejoint un groupe. J'ai presque fini de peaufiner une bibliothèque personnalisée pour cela. Je ne l'ai pas encore publié mais il est fonctionnel! Je vais essayer de poster un lien bientôt. [~ # ~] éditer [~ # ~] J'ai fini par transformer tous mes pivots en modèles normaux et ma vie a été tellement plus facile ...

Répondre aux préoccupations des gens avec l'utilisation de modèles:

  • Organisation: Oui, si vous incluez plus de logique dans les modèles, ils peuvent être plus longs, mais en général, j'ai trouvé que 75% de mes modèles sont encore assez petits. . Si je choisis d'organiser les plus gros, je peux le faire en utilisant des traits (par exemple, créez un dossier pour le modèle avec quelques fichiers supplémentaires tels que PostScopes, PostAccessors, PostValidation, etc. si nécessaire). Je sais que ce n'est pas nécessairement à quoi servent les traits, mais ce système fonctionne sans problème.

Remarque additionnelle: J'ai l'impression que le fait d'envelopper vos modèles dans les services équivaut à disposer d'un couteau suisse, doté de nombreux outils, et à construire un autre couteau autour de ce modèle. fait la même chose? Oui, parfois vous voudrez peut-être scotcher une lame ou vous assurer que deux lames sont utilisées ensemble ... mais il y a généralement d'autres façons de le faire ...

QUAND UTILISER DES SERVICES : Cet article énonce très bien de très bons exemples d'utilisation des services (, indice: ce n'est pas très souvent ). Il dit essentiellement lorsque votre objet utilise plusieurs modèles ou des modèles à des moments étranges de leur cycle de vie cela a du sens. http://www.justinweiss.com/articles/where-do-you-put-your-code/

19
Sabrina Leggett

Pour créer la logique entre les contrôleurs et les modèles, je crée une couche de service . Fondamentalement, ceci est mon flux pour toute action au sein de mon application:

  1. Le contrôleur obtient l'action demandée par l'utilisateur, les paramètres envoyés et le tout délégué à une classe de service.
  2. La classe de service effectue toute la logique liée à l'opération: validation des entrées, journalisation des événements, opérations de base de données, etc.
  3. Model contient des informations sur les champs, la transformation des données et les définitions des validations d'attributs.

Voici comment je le fais:

C'est la méthode d'un contrôleur pour créer quelque chose:

public function processCreateCongregation()
{
    // Get input data.
    $congregation                 = new Congregation;
    $congregation->name           = Input::get('name');
    $congregation->address        = Input::get('address');
    $congregation->pm_day_of_week = Input::get('pm_day_of_week');
    $pmHours                      = Input::get('pm_datetime_hours');
    $pmMinutes                    = Input::get('pm_datetime_minutes');
    $congregation->pm_datetime    = Carbon::createFromTime($pmHours, $pmMinutes, 0);

    // Delegates actual operation to service.
    try
    {
        CongregationService::createCongregation($congregation);
        $this->success(trans('messages.congregationCreated'));
        return Redirect::route('congregations.list');
    }
    catch (ValidationException $e)
    {
        // Catch validation errors thrown by service operation.
        return Redirect::route('congregations.create')
            ->withInput(Input::all())
            ->withErrors($e->getValidator());
    }
    catch (Exception $e)
    {
        // Catch any unexpected exception.
        return $this->unexpected($e);
    }
}

C'est la classe de service qui effectue la logique liée à l'opération:

public static function createCongregation(Congregation $congregation)
{
    // Log the operation.
    Log::info('Create congregation.', compact('congregation'));

    // Validate data.
    $validator = $congregation->getValidator();

    if ($validator->fails())
    {
        throw new ValidationException($validator);
    }

    // Save to the database.
    $congregation->created_by = Auth::user()->id;
    $congregation->updated_by = Auth::user()->id;

    $congregation->save();
}

Et voici mon modèle:

class Congregation extends Eloquent
{
    protected $table = 'congregations';

    public function getValidator()
    {
        $data = array(
            'name' => $this->name,
            'address' => $this->address,
            'pm_day_of_week' => $this->pm_day_of_week,
            'pm_datetime' => $this->pm_datetime,
        );

        $rules = array(
            'name' => ['required', 'unique:congregations'],
            'address' => ['required'],
            'pm_day_of_week' => ['required', 'integer', 'between:0,6'],
            'pm_datetime' => ['required', 'regex:/([01]?[0-9]|2[0-3]):[0-5]?[0-9]:[0-5][0-9]/'],
        );

        return Validator::make($data, $rules);
    }

    public function getDates()
    {
        return array_merge_recursive(parent::getDates(), array(
            'pm_datetime',
            'cbs_datetime',
        ));
    }
}

Pour plus d'informations sur cette méthode, j'organise mon code pour un Laravel app: https://github.com/rmariuzzo/Pitimi

17
Rubens Mariuzzo

À mon avis, Laravel dispose déjà de nombreuses options pour stocker votre logique métier.

Réponse courte:

  • Utilisez les objets Request de laravel pour valider automatiquement votre entrée, puis persistez les données dans la demande (créez le modèle). Étant donné que toutes les entrées d'utilisateurs sont directement disponibles en la demande, je pense qu'il est logique de le faire ici.
  • Utilisez les objets Job de laravel pour effectuer des tâches nécessitant des composants individuels, puis envoyez-les simplement. Je pense que Job englobe les classes de services. Ils effectuent une tâche, telle que la logique métier.

Réponse plus longue:

tilisez des réserves si nécessaire: Les référentiels sont forcément surchargés, et la plupart du temps, ils sont simplement utilisés en tant que accessor dans le modèle. Je pense qu’ils ont certainement une utilité, mais à moins que vous ne développiez une application massive qui nécessite cette souplesse vous permet de laisser tomber laravel entièrement, éloignez-vous des dépôts, vous vous en remercierez plus tard et votre code sera beaucoup plus simple.

Demandez-vous s'il est possible que vous changiez PHP frameworks o => en un type de base de données qui laravel ne fait pas ' t soutien.

Si votre réponse est "Probablement pas", n'implémentez pas le modèle de référentiel.

En plus de ce qui précède, ne claquez pas un motif sur un superbe ORM comme Eloquent. Vous ajoutez simplement une complexité qui n'est pas requise et qui ne vous profitera pas du tout.

Pour moi, utiliser les services avec parcimonie: Les classes de services ne sont qu'un emplacement dans lequel stocker la logique applicative pour effectuer une tâche spécifique avec ses dépendances données. Laravel a ces options prédéfinies, appelées "Jobs", et elles ont beaucoup plus de flexibilité qu'une classe de service personnalisée.

Je pense que Laravel a une solution complète pour résoudre le problème de logique MVC. Il s’agit simplement d’une question ou d’une organisation.

Exemple:

demande:

namespace App\Http\Requests;

use App\Post;
use App\Jobs\PostNotifier;
use App\Events\PostWasCreated;
use App\Http\Requests\Request;

class PostRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title'       => 'required',
            'description' => 'required'
        ];
    }

    /**
     * Save the post.
     *
     * @param Post $post
     *
     * @return bool
     */
    public function persist(Post $post)
    {
        if (!$post->exists) {
            // If the post doesn't exist, we'll assign the
            // post as created by the current user.
            $post->user_id = auth()->id();
        }

        $post->title = $this->title;
        $post->description = $this->description;

        // Perform other tasks, maybe fire an event, dispatch a job.

        if ($post->save()) {
            // Maybe we'll fire an event here that we can catch somewhere else that
            // needs to know when a post was created.
            event(new PostWasCreated($post));

            // Maybe we'll notify some users of the new post as well.
            dispatch(new PostNotifier($post));

            return true;
        }

        return false;
    }
}

Contrôleur:

namespace App\Http\Controllers;

use App\Post;
use App\Http\Requests\PostRequest;

class PostController extends Controller
{

   /**
    * Creates a new post.
    *
    * @return string
    */
    public function store(PostRequest $request)
    {
        if ($request->persist(new Post())) {
            flash()->success('Successfully created new post!');
        } else {
            flash()->error('There was an issue creating a post. Please try again.');
        }

        return redirect()->back();
    }

   /**
    * Updates a post.
    *
    * @return string
    */
    public function update(PostRequest $request, $id)
    {
        $post = Post::findOrFail($id);

        if ($request->persist($post)) {
            flash()->success('Successfully updated post!');
        } else {
            flash()->error('There was an issue updating this post. Please try again.');
        }

        return redirect()->back();
    }
}

Dans l'exemple ci-dessus, l'entrée de la demande est automatiquement validée et il suffit d'appeler la méthode persist et de transmettre une nouvelle publication. Je pense que la lisibilité et la maintenabilité doivent toujours primer sur les modèles de conception complexes et inutiles.

Vous pouvez ensuite utiliser exactement la même méthode de persistance pour mettre à jour les publications, car nous pouvons vérifier si la publication existe déjà ou non et effectuer une logique alternative en cas de besoin.

9
Steve Bauman