ChatGPT解决这个技术问题 Extra ChatGPT

MVC (Laravel) where to add logic

Let's say whenever I do a CRUD operation or modify a relationship in a specific way I also want to do something else. E.g., whenever someone publishes a post I also want to save something to a table for analytics. Maybe not the best example but in general there's a lot of this "grouped" functionality.

Normally I see this type of logic put into controllers. That's all fine an dandy until you want to reproduce this functionality in lots of places. When you start getting into partials, creating an API and generating dummy content it becomes an issue with keeping things DRY.

The ways I've seen to manage this are events, repositories, libraries, and adding to models. Here are my understandings of each:

Services: This is where most people would probably put this code. My main issue with services is that sometimes it's hard to find specific functionality in them and I feel like they get forgotten about when people are focused on using Eloquent. How would I know I need to call a method publishPost() in a library when I can just do $post->is_published = 1?

The only condition I see this working well in is if you ONLY use services (and ideally make Eloquent inaccessible somehow from controllers all together).

Ultimately it seems like this would just create a bunch of extra unnecessary files if your requests generally follow your model structure.

Repositories: From what I understand this is basically like a service but there's an interface so you can switch between ORMs, which I don't need.

Events: I see this as the most elegant system in a sense because you know your model events are always going to be called on Eloquent methods, so you can write your controllers like you normally would. I can see these getting messy though and if anyone has examples of large projects using events for critical coupling I'd like to see it.

Models: Traditionally I'd have classes that performed CRUD and also handled critical coupling. This actually made things easy because you knew all functionality around CRUD + whatever had to be done with it was there.

Simple, but in MVC architecture this isn't normally what I see done. In a sense though I prefer this over services since it's a bit easier to find, and there are less files to keep track of. It can get a bit disorganized though. I'd like to hear downfalls to this method and why most people don't seem to do it.

What are the advantages / disadvantages of each method? Am I missing something?

Can you minimize your question ?
Also you may check this.
"How would I know I need to call a method publishPost() in a library when I can just do $post->is_published = 1?" Documentation?
one of the beauties about eloquent and ORMS is it's easier to work with them without lots of docs?
Thanks for posting this. I am struggle with the same issues and found your post and answer incredibly helpful. Ultimately I have decided that Laravel does not provide a good architecture for anything that stretches beyond a quick and dirty Ruby-on-Rails website. Trates everywhere, difficulty finding classes functions and tons of auto-magic garbage everywhere. ORM has never worked and if you're using it, you should probably be using NoSQL.

s
samrap

I think all patterns / architectures that you present are very useful as long as you follow the SOLID principles.

For the where to add logic I think that it's important to refer to the Single Responsibility Principle. Also, my answer considers that you are working on a medium / large project. If it's a throw-something-on-a-page project, forget this answer and add it all to controllers or models.

The short answer is: Where it makes sense to you (with services).

The long answer:

Controllers: What is the responsibility of Controllers? Sure, you can put all your logic in a controller, but is that the controller's responsibility? I don't think so.

For me, the controller must receive a request and return data and this is not the place to put validations, call db methods, etc..

Models: Is this a good place to add logic like sending an welcome email when a user registers or update the vote count of a post? What if you need to send the same email from another place in your code? Do you create a static method? What if that emails needs information from another model?

I think the model should represent an entity. With Laravel, I only use the model class to add things like fillable, guarded, table and the relations (this is because I use the Repository Pattern, otherwise the model would also have the save, update, find, etc methods).

Repositories (Repository Pattern): At the beginning I was very confused by this. And, like you, I thought "well, I use MySQL and thats that.".

However, I have balanced the pros vs cons of using the Repository Pattern and now I use it. I think that now, at this very moment, I will only need to use MySQL. But, if three years from now I need to change to something like MongoDB most of the work is done. All at the expense of one extra interface and a $app->bind(«interface», «repository»).

Events (Observer Pattern): Events are useful for things that can be thrown at any class any given time. Think, for instance, of sending notifications to a user. When you need, you fire the event to send a notification at any class of your application. Then, you can have a class like UserNotificationEvents that handles all of your fired events for user notifications.

Services: Until now, you have the choice to add logic to controllers or models. For me, it makes all sense to add the logic within Services. Let's face it, Services is a fancy name for classes. And you can have as many classes as it makes sense to you within your aplication.

Take this example: A short while ago, I developed something like the Google Forms. I started with a CustomFormService and ended up with CustomFormService, CustomFormRender, CustomFieldService, CustomFieldRender, CustomAnswerService and CustomAnswerRender. Why? Because it made sense to me. If you work with a team, you should put your logic where it makes sense to the team.

The advantage of using Services vs Controllers / Models is that you are not constrained by a single Controller or a single Model. You can create as many services as needed based on the design and needs of your application. Add to that the advantage of calling a Service within any class of your application.

This goes long, but I would like to show you how I have structured my application:

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

I use each folder for a specific function. For example the Validators directory contains a BaseValidator class responsible for processing the validation, based on the $rules and $messages of specific validators (usually one for each model). I could as easily put this code within a Service, but it makes sense to me to have a specific folder for this even if it is only used within the service (for now).

I recommend you to read the following articles, as they might explain things a little better to you:

Breaking the Mold by Dayle Rees (author of CodeBright): This is where I put it all together, even though I changed a few things to fit my needs.

Decoupling your code in Laravel using Repositories and Services by Chris Goosey: This post explains well what is a Service and the Repository Pattern and how they fit together.

Laracasts also have the Repositories Simplified and Single Responsibility which are good resources with practical examples (even though you have to pay).


great explanation. Here's kinda where I'm standing at the moment - in the current project I'm putting my business logic in models and it's actually working very well. We definitely need to fudge SOLID a little bit, but, it hasn't really gotten us into trouble yet. It's fast, it's kinda dirty, but so far our project is very maintainable because it's so DRY. I'm definitely ok with sticking to them at the moment because they get the job done, but in any future project I'll probably just go with whatever is standard, which it sounds like repositories have become.
I'm glad you found a way that makes sense to you. Just be careful with the assumptions that you make today. I've worked on a project for 3+ years and ended up with controllers and models with 5000+ lines of code. Good luck with your project.
also kinda dirty but I was thinking about using traits to avoid models getting huge. That way I can separate them out a bit
This article articulates well WHEN it makes sense to use services. In your Form example it does make sense to use services, but he expalins how he does it, which is when the logic is directly related to a model he puts it in that model. justinweiss.com/articles/where-do-you-put-your-code
I really like the explanation. There are one question I have: you mentioned not to put validation in controller, so where do you think is the best place to do validation? Many suggest to put it into extended Request class (and also what we currently do), but what if I not just want to validate on http request, but also on artisan command, etc, is it really a good place?
S
Sabrina Leggett

I wanted to post a response to my own question. I could talk about this for days, but I'm going to try to get this posted fast to make sure I get it up.

I ended up utilizing the existing structure that Laravel provides, meaning that I kept my files primarily as Model, View, and Controller. I also have a Libraries folder for reusable components that aren't really models.

I DID NOT WRAP MY MODELS IN SERVICES/LIBRARIES. All of the reasons provided didn't 100% convince me of the benefit of using services. While I may be wrong, as far as I can see they just result in tons of extra nearly empty files I need to create and switch between when working with models and also really reduce the benefit of using eloquent (especially when it comes to RETRIEVING models, e.g., using pagination, scopes, etc).

I put the business logic IN THE MODELS and access eloquent directly from my controllers. I use a number of approaches to make sure that the business logic doesn't get bypassed:

Accessors and mutators: Laravel has great accessors and mutators. If I want to perform an action whenever a post is moved from draft to published I can call this by creating function setIsPublishedAttribute and including the logic in there

Overriding Create/Update etc: You can always override Eloquent methods in your models to include custom functionality. That way you can call functionality on any CRUD operation. Edit: I think there's a bug with overriding create in newer Laravel versions (so I use events now registered in boot)

Validation: I hook my validation in the same way, e.g., I'll run validation by overriding CRUD functions and also accessors/mutators if needed. See Esensi or dwightwatson/validating for more information.

Magic Methods: I use the __get and __set methods of my models to hook into functionality where appropriate

Extending Eloquent: If there's an action you'd like to take on all update/create you can even extend eloquent and apply it to multiple models.

Events: This is a straight forward and generally agreed upon place to do this as well. Biggest drawback with events I think is that exceptions are hard to trace (might not be the new case with Laravel's new events system). I also like to group my events by what they do instead of when they are called...e.g., have a MailSender subscriber which listens for events that send mail.

Adding Pivot/BelongsToMany Events: One of the things I struggled with the longest was how to attach behavior to the modification of belongsToMany relationships. E.g., performing an action whenever a user joins a group. I'm almost done polishing up a custom library for this. I haven't published it yet but it is functional! Will try to post a link soon. EDIT I ended up making all my pivots into normal models and my life has been so much easier...

Addressing people's concerns with using models:

Organization: Yes if you include more logic in models, they can be longer, but in general I've found 75% of my models are still pretty small. If I chose to organize the larger ones I can do it using traits (e.g., create a folder for the model with some more files like PostScopes, PostAccessors, PostValidation, etc as needed). I know this is not necessarily what traits are for but this system works without issue.

Additional Note: I feel like wrapping your models in services is like having a swiss army knife, with lots of tools, and building another knife around it that basically does the same thing? Yeah, sometimes you might want to tape a blade off or make sure two blades are used together...but there are typically other ways to do it...

WHEN TO USE SERVICES: This article articulates very well GREAT examples for when to use services (hint: it's not very often). He says basically when your object uses multiple models or models at strange parts of their lifecycle it makes sense. http://www.justinweiss.com/articles/where-do-you-put-your-code/


Interesting and valid thoughts. But I'm curious - how do you unit-test your business logic if it is tied to models which are tied to Eloquent, which is tied to database?
code.tutsplus.com/tutorials/… or you can use events like I said if you want to break it down further
@JustAMartin are you sure that you can't just use database in your unit tests? What is the reason not to do it? Many people agree that often it is okay to use database in unit tests. (including Martin Fowler, martinfowler.com/bliki/UnitTest.html: "I don't treat using doubles for external resources as an absolute rule. If talking to the resource is stable and fast enough for you then there's no reason not to do it in your unit tests")
@AlexP11223 Yes, that makes sense. I tried to integrate SQLite as my test database and in general it went fine, although SQLite has some serious limitations which have to accounted for in Laravel migrations and custom queries (if any). Of course, those then are not strictly unit tests but functional tests, but it's even more efficient that way. Still, if you want to test your model in complete isolation (as a strict unit test), it might require noticeable amount of additional code (mocks etc.).
Sounds to me like great thoughts a project with less than 100 controllers making calls to the data access layer. For projects larger than this, I strongly encourage passing all data access calls through a service layer. This ensures that as requirements change, which they always do, there is a "single point" in the code where all calls to "get x type of data" are handled. You do not want to hunt down 50+ calls to that model and figure out if it is being used in a way that is impacted by the new business rule or not.
A
Alex P.

What I use to do to create the logic between controllers and models is to create a service layer. Basically, this is my flow for any action within my app:

Controller get user's requested action and sent parameters and delegates everything to a service class. Service class do all the logic related to the operation: input validation, event logging, database operations, etc... Model holds information of fields, data transformation, and definitions of attributes validations.

This is how I do it:

This the method of a controller to create something:

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);
    }
}

This is the service class that does the logic related to the operation:

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();
}

And this is my model:

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',
        ));
    }
}

For more information about this way I use to organize my code for a Laravel app: https://github.com/rmariuzzo/Pitimi


It seems like services are what I referred to as libraries in my post. I think that this is the better than repositories if you don't need to use multiple ORMS, but the issue is that you have to migrate your whole project over (which you don't have to do with events), and it seems like it kinda just ends up mirroring the Model structure so you've just got all these extra files. Why not just include it in the models? At least that way you don't have the extra files.
That's an interesting question @SabrinaGelbart, I have been taught to let models represent the database entities and to not hold any logic. That's the reason what I created those extra files named as services: to hold all the logic and any extra operations. I'm not sure what is the whole meaning of events you described before, but I think with services and using Laravel's events we can make all services methods to fire events at start and end. This way any event can be completely decoupled from logic. What do you think?
I was taught that too about models...would be nice to get a good explanation for why (maybe dependency issues)?
I like this approach! I have been searching internet to get an idea of how i should be handling model's logic, looked over Repositories but it seemed too complicated and useless for a bit of usage. Services is a good idea. My question is after creating a folder of Services in app folder, do you have to include it in bootstrap/start.php or anywhere for booting because i looked over your git couldnt find it? @RubensMariuzzo. Does it automatically become available thorughout the app ? so we can just use CongregationService::getCongregations(); ??
If all you are doing is a $congregation->save(); then maybe you wouldn't need Repositories. However, you might see your data access needs increase over time. You may start to have needs for $congregation->destroyByUser() or $congregationUsers->findByName($arrayOfSelectedFields); and so on. Why not de-couple your services from the data access needs. Let the rest of your app work with objects/arrays returned from repos, and just handle manipulating/formatting/etc... Your repo's will grow (but split them into different files, ultimately the complexity of a project has to reside somewhere).
S
Steve Bauman

In my opinion, Laravel already has many options for you to store your business logic.

Short answer:

Use Laravel's Request objects to automatically validate your input, and then persist the data in the request (create the model). Since all of the users input is directly available in the request, I believe it makes sense to perform this here.

Use Laravel's Job objects to perform tasks that require individual components, then simply dispatch them. I think Job's encompass service classes. They perform a task, such as business logic.

Long(er) answer:

Use Respositories When Required: Repositories are bound to be over-bloated, and most of the time, are simply used as an accessor to the model. I feel like they definitely have some use, but unless you're developing a massive application that requires that amount of flexibility for you to be able to ditch Laravel entirely, stay away from repositories. You'll thank yourself later and your code will be much more straight forward.

Ask yourself if there's a possibility that you're going to be changing PHP frameworks or to a database type that Laravel doesn't support.

If your answer is "Probably not", then don't implement the repository pattern.

In addition to above, please don't slap a pattern on top of a superb ORM like Eloquent. You're just adding complexity that isn't required and it won't benefit you at all.

Utilize Services sparingly: Service classes to me, are just a place to store business logic to perform a specific task with its given dependencies. Laravel has these out of the box, called 'Jobs', and they have much more flexibility than a custom Service class.

I feel like Laravel has a well-rounded solution for the MVC logic problem. It's just a matter or organization.

Example:

Request:

namespace App\Http\Requests;

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

class PostRequest extends Request
{
    public function rules()
    {
        return [
            'title'       => 'required',
            'description' => 'required'
        ];
    }

    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;

        $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 $post;
    }
}

Controller:

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    public function store(PostRequest $request)
    {
        $request->persist(new Post());

        flash()->success('Successfully created new post!');
        
        return redirect()->back();
    }

    public function update(PostRequest $request, Post $post)
    {
        $request->persist($post);

        flash()->success('Successfully updated post!');
        
        return redirect()->back();
    }
}

In the example above, the request input is automatically validated, and all we need to do is call the persist method and pass in a new Post. I think readability and maintainability should always trump complex and unneeded design patterns.

You can then utilize the exact same persist method for updating posts as well, since we can check whether or not the post already exists and perform alternating logic when needed.


but - aren't jobs "supposed" to be queued? Sometimes we probably do want it to be queued but not all the time. Why not use commands instead? What if you want to write some business logic that may be executed as a command or an event or a queued?
Jobs don't need to be queued. You specify that by implementing the interface on the job ShouldQueue that Laravel provides. If you want to write Business logic in a command or event, just fire the job inside those events / commands. Laravels jobs are extremely flexible, but in the end they are just plain service classes.
Very good point! Also jobs can be used immediately by the dispatchSync() method which should ignore ShouldQueue.