Home>Article>PHP Framework> The charm of Laravel single-behavior controller design
Yesterday, Jeffrey Way posted a tweet asking people if they would rather name their controllers singular or plural. I replied that I didn't choose either option, I used a single action controller. What happens next is that some people agree, some don't, and some do the weirdest things.
Due to the overwhelming response, I wanted to write an article explaining why I love single action controllers and why I find them wonderful.
First of all, before starting the article, I want to say that this thing is not a single truth. As always, I want to point out that it all comes down to your personal preference. I can only teach, suggest and point out things and it's up to you to agree, disagree, accept, learn and/or adjust. Or neither. Take what you want from this blog and just do whatever makes you feel comfortable.
Comparison between CRUD and Domain Modelling
Before we begin, let’s first think about our tendency to write resourceful CRUD controllers. I'm sure many people will stick with this approach since it's a standard practice in Laravel and most of the examples in the documentation use this approach. In addition, this may also be something you often see in various blogs or app code.
But if you stop and think about it, is this the best way to write them? Is this a common practice in the software industry? In recent years, I've spent a lot of time in areas like Domain Driven Design and thinking about how software applies to the domain (Domian) you work in and how it translates. When you start thinking about terminology and phrasing that mimics the ubiquitous language in your field, you'll find that your code will become clearer and more to the point. (This last sentence is still worthy of consideration and improvement)
Finally, I believe that the essence of writing software is to use domain processes as much as possible to make your code more readable and maintainable.
Resourceful controller does not do these two aspects well. First, they are less readable because you tend to structure them in terms of the data rather than in terms of the domain. In this case, you lose contextual control. You show how the data is processed, but you don't explain what exactly happens, or which process you use to process it.
Second, you are not optimizing for maintainability. Since you're building in terms of data structures, you're also coupling into them. In fact, your domain model is constantly evolving, and so are your data structures. If your data structure handles multiple processes or multiple parts of the domain, it will be difficult to adjust.
A practical example
Because theory is boring and code is easier to explain, so let’s look at a practical example.
Suppose you are building an application that allows users to organize events. You want to provide a way to create, update and delete these events. This is a very typical example of how you would think about implementing it in CRUD terms. So, let's see how such a resourceful controller is transformed.
First let’s take a look at routing:
Route::get('events', [EventController::class, 'index']); Route::get('events/create', [EventController::class, 'create']); Route::post('events', [EventController::class, 'store']); Route::get('event/{event}', [EventController::class, 'show']); Route::get('events/{event}/edit', [EventController::class, 'edit']); Route::put('events/{event}', [EventController::class, 'update']); Route::destroy('events/{event}', [EventController::class, 'destroy']);
Now the corresponding controller:
This EventController handles all CRUD requests, displays the event list, and displays the specified event. Create an event, update an existing event and delete an event.
Let’s take a look at the details of the index method:
public function index() { $events = Event::paginate(10); return view('events.index', compact('events')); }In this method, we retrieve the events and then give them to the view to display them in a paging list. So far so good. But now you want to implement a method to use different pages to view past and upcoming events. Let's see how to implement it in the index method:
public function index(Request $request) { if ($request->boolean('past')) { $events = Event::past()->paginate(10); } elseif ($request->boolean('upcoming')) { $events = Event::upcoming()->paginate(10); } else { $events = Event::paginate(10); } return view('events.index', compact('events')); }Ugh! It looks so messy. Although we have used Eloquent scopes to hide the query logic, we still have ugly chained statements. Let's see how to replace this with a single behavior controller.
Each single-behavior controller only performs one thing, only one thing.
First, instead of using query parameters to get different event lists, we use dedicated routing to achieve it.
Route::get('events', ShowAllEventsController::class); Route::get('events/past', ShowPastEventsController::class); Route::get('events/upcoming', ShowUpcomingEventsController::class);This route is longer than the previous one, but this one is more expressive than the previous one. You can identify at a glance which controller handles which specific logic. If you compare the URLs, you'll see some improvement in readability:
# Before /events /events?past=true /events?upcoming=true # After /events /events/past /events/upcomingNow look at one of the controllers. Just look at the ShowUpcomingEventsController controller:
paginate(10); return view('events.index', compact('events')); } }The ugly if statement is gone, and has made way for the same readable three liner we had from our first CRUD controller example. But instead of having all of the other CRUD operations we now have a dedicated controller for a dedicated action.
Simple, easy to read, and easy to maintain.
你可能会问自己,这样做值么,毕竟之前的 if 语句也没那么坏吧?但是我想向你展示的是你正在为未来的改进做优化,并改进维护性。下次你想要对这三个页面做任何指定改变的时候,你会知道在哪里改,并且不需要艰难地更新一个 if 语句。
当然,上面的例子很简单,我们来看一个更复杂一点的。我们试试重构 create 和 store 方法:
public function create() { return view('events.create'); } public function store(Request $request) { $data = $request->validate([ 'name' => 'required', 'start' => 'required', 'end' => 'required|after:start', ]) $event = Event::create($data); return redirect()->route('event.show', $event); }我们要做的就是把这两个方法移到专用的控制器,这样更好地解释了这些方法做了啥。这些方法更好地服务于你,比起把它们放在一个叫做 ScheduleNewEventController 的控制器中。我们接着更新这个控制器的路由:
Route::get('events/schedule', [ScheduleNewEventController::class, 'showForm']); Route::post('events/schedule', [ScheduleNewEventController::class, 'schedule']);我不会向你展示一个确切的控制器,因为它们有和上面的例子一样,有两个方法,只不过把 showForm 和 schedule 重新命名为更能表达它们干了啥的名字。即使这个不是单行为控制器,但是方法论是一样的:把你应用中的专用行为(方法)和它对应的控制器拆分到一起。
好了,现在你已经看了单行为控制器的例子了。你可能会想,这会导致越来越多的文件。但事实上,这个根本就不是问题。文件多又没啥。有更多、更小、更容易维护的文件比有更大、更难分析的要好。你可以打开一个单行为控制器的文件,然后快速扫描代码,马上就能知道这是干嘛的。
我经常把他们分组到不同的目录,这些目录负责领域的各个部分。这让你从文件结构的角度看控制器时,更加容易。
拆分控制器也让你跟容易找到特定的一个控制器。想象一下,你要寻找那个可以安排事件的控制器时。现在你只需要按照文件名搜索编辑器,而不是一个通用的 EventController。
其他情况
我也被问到是否要对所有控制器执行此操作。不总是。在命名控制器时,我倾向于严谨且简洁,但我也会像你一样适应各种情况。
当然,有时候你还是想用 resourceful 控制器。比如在你构建 RESTful API 时。这样做是很有意义,因为你经常直接与数据本身交互,而没有经常与领域或任何进程进行交互。CMS(内容管理系统)或 Laravel Nova 等应用程序就是最好的例子。
但是在需要的时候,您最好问问自己的方案是否更接近领域和处理过程。在需要根据领域执行操作的时候,比如 GraphQL 之类的或 API 之类的 RPC ,这样做可能更适合。
结论
我希望这有一点见地,你现在能更理解我为什么如此喜欢单行为控制器了吧。我相信,结合小的 classes,再使用无处不在的语言、显式地命名,会带来更可维护的代码,甚至是控制器,不仅仅是领域对象。但是正如我开头所说,选择能帮助你的部分,好好分辨哪些适用于你,哪些不行。
The above is the detailed content of The charm of Laravel single-behavior controller design. For more information, please follow other related articles on the PHP Chinese website!