L

Laravel Service Classes Explained Table of Content Project Codes The Scenario Understanding Business Logic Service Classes to the Rescue Action Re-usability Closing Thoughts

2025-06-08

Laravel 服务类详解

目录

项目代码

场景

理解业务逻辑

救援服务类

动作可重用性

结束语

本杰明·富兰克林曾经说过——

凡事皆有其所,各得其位。

这也适用于软件开发。了解代码的各部分应该放在哪里是维护代码库的关键。

Laravel是一个优雅的 Web 框架,默认带有一个非常有组织的目录结构,但我仍然看到很多人为此苦恼。

别误会我的意思。控制器放在目录里是理所当然的controllers,一点也不令人困惑。人们经常困惑的是,控制器里应该写什么,不应该写什么。

目录

项目代码

您可以在以下存储库中找到本文讨论的服务的实现:

GitHub 徽标 fhsinchy / laravel-livewire-购物车

由 Laravel、Livewire 和 TailwindCSS 提供支持的实时购物车

除了 Laravel 之外,该项目还使用了LivewireTailwindCSS

场景

以下面一段代码为例:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CartItemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $content = session()->has('cart') ? session()->get('cart') : collect([]);
        $total = $content->reduce(function ($total, $item) {
            return $total += $item->get('price') * $item->get('quantity');
        });

        return view('cart.index', [
            'content' => $content,
            'total' => $total,
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string',
            'price' => 'required|numeric',
            'quantity' => 'required|integer',
        ]);

        $cartItem = collect([
            'name' => $request->name,
            'price' => floatval($request->price),
            'quantity' => intval($request->quantity),
            'options' => $request->options,
        ]);

        $content = session()->has('cart') ? session()->get('cart') : collect([]);

        $id = request('id');

        if ($content->has($id)) {
            $cartItem->put('quantity', $content->get($id)->get('quantity') + $request->quantity);
        }

        $content->put($id, $cartItem);

        session()->put('content', $content);

        return back()->with('success', 'Item added to cart');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $content = session()->get('cart');

        if ($content->has($id)) {
            $item = $content->get($id);

            return view('cart', compact('item'));
        }

        return back()->with('fail', 'Item not found');
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $content = session()->get('cart');

        if ($content->has($id)) {
            $cartItem = $content->get($id);

            switch ($request->action) {
                case 'plus':
                    $cartItem->put('quantity', $content->get($id)->get('quantity') + 1);
                    break;
                case 'minus':
                    $updatedQuantity = $content->get($id)->get('quantity') - 1;

                    if ($updatedQuantity < 1) {
                        $updatedQuantity = 1;
                    }

                    $cartItem->put('quantity', $updatedQuantity);
                    break;
            }

            $content->put($id, $cartItem);

            session()->put('cart', $content);

            return back()->with('success', 'Item updated in cart');
        }

        return back()->with('fail', 'Item not found');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $content = session()->get('cart');

        if ($content->has($id)) {
            session()->put('cart', $content->except($id));

            return back()->with('success', 'Item removed from cart');
        }

        return back()->with('fail', 'Item not found');
    }
}
Enter fullscreen mode Exit fullscreen mode

这是一个虚构的电商项目中的控制器,负责管理购物车。虽然这是一段完全有效的代码,但也存在一些问题。

控制器也知道太多了。以index方法为例,它不需要知道购物车内容是来自会话还是数据库。它也不需要知道如何计算购物车商品的总价。

控制器应该只负责传输请求和响应。内部细节,也就是业务逻辑,应该由服务器中的其他类来处理。

理解业务逻辑

正如本主题所解释的

业务逻辑或领域逻辑是程序的一部分,它编码了现实世界的业务规则,这些规则决定了如何创建、存储和更改数据。它规定了业务对象之间的交互方式,并强制执行访问和更新业务对象的路由和方法。

对于简单的购物车系统,将商品添加到购物车背后的业务逻辑可以描述如下:

  1. 将必要的产品信息(id、名称、价格、数量)作为输入。
  2. 验证输入数据。
  3. 形成一个新的购物车商品。
  4. 检查该商品是否已存在于购物车中。
  5. 如果是,则更新其数量,如果不是,则将新形成的商品添加到购物车。

现在让我们看一下该store方法:

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        // validate the input data.
        $request->validate([
            'name' => 'required|string',
            'price' => 'required|numeric',
            'quantity' => 'required|integer',
        ]);

        // form a new cart item.
        $cartItem = collect([
            'name' => $request->name,
            'price' => floatval($request->price),
            'quantity' => intval($request->quantity),
            'options' => $request->options,
        ]);

        // check if the item already exists in the cart.
        $content = session()->has('cart') ? session()->get('cart') : collect([]);
        $id = request('id');
        if ($content->has($id)) {
            // if yes, update it's quantity
            $cartItem->put('quantity', $content->get($id)->get('quantity') + $request->quantity);
        }

        // if no, add the newly formed item to cart.
        $content->put($id, $cartItem);
        $this->session->put('content', $content);

        return back()->with('success', 'Item added to cart');
    }
Enter fullscreen mode Exit fullscreen mode

如你所见,业务逻辑相当准确地转换成了代码。现在的问题是,控制器不应该包含业务逻辑。

救援服务类

根据非常流行的alexeymezenin/laravel-best-practices存储库:

业务逻辑应该在服务类中

服务类的概念并非框架内置,官方文档中也未记录。因此,不同的人对它们的称呼也有所不同。归根结底,服务类是负责承载业务逻辑的普通类。

一个用于保存购物车相关业务逻辑的服务类可以如下:

<?php

namespace App\Services;

use Illuminate\Support\Collection;
use Illuminate\Session\SessionManager;

class CartService {
    const MINIMUM_QUANTITY = 1;
    const DEFAULT_INSTANCE = 'shopping-cart';

    protected $session;
    protected $instance;

    /**
     * Constructs a new cart object.
     *
     * @param Illuminate\Session\SessionManager $session
     */
    public function __construct(SessionManager $session)
    {
        $this->session = $session;
    }

    /**
     * Adds a new item to the cart.
     *
     * @param string $id
     * @param string $name
     * @param string $price
     * @param string $quantity
     * @param array $options
     * @return void
     */
    public function add($id, $name, $price, $quantity, $options = []): void
    {
        $cartItem = $this->createCartItem($name, $price, $quantity, $options);

        $content = $this->getContent();

        if ($content->has($id)) {
            $cartItem->put('quantity', $content->get($id)->get('quantity') + $quantity);
        }

        $content->put($id, $cartItem);

        $this->session->put(self::DEFAULT_INSTANCE, $content);
    }

    /**
     * Updates the quantity of a cart item.
     *
     * @param string $id
     * @param string $action
     * @return void
     */
    public function update(string $id, string $action): void
    {
        $content = $this->getContent();

        if ($content->has($id)) {
            $cartItem = $content->get($id);

            switch ($action) {
                case 'plus':
                    $cartItem->put('quantity', $content->get($id)->get('quantity') + 1);
                    break;
                case 'minus':
                    $updatedQuantity = $content->get($id)->get('quantity') - 1;

                    if ($updatedQuantity < self::MINIMUM_QUANTITY) {
                        $updatedQuantity = self::MINIMUM_QUANTITY;
                    }

                    $cartItem->put('quantity', $updatedQuantity);
                    break;
            }

            $content->put($id, $cartItem);

            $this->session->put(self::DEFAULT_INSTANCE, $content);
        }
    }

    /**
     * Removes an item from the cart.
     *
     * @param string $id
     * @return void
     */
    public function remove(string $id): void
    {
        $content = $this->getContent();

        if ($content->has($id)) {
            $this->session->put(self::DEFAULT_INSTANCE, $content->except($id));
        }
    }

    /**
     * Clears the cart.
     *
     * @return void
     */
    public function clear(): void
    {
        $this->session->forget(self::DEFAULT_INSTANCE);
    }

    /**
     * Returns the content of the cart.
     *
     * @return Illuminate\Support\Collection
     */
    public function content(): Collection
    {
        return is_null($this->session->get(self::DEFAULT_INSTANCE)) ? collect([]) : $this->session->get(self::DEFAULT_INSTANCE);
    }

    /**
     * Returns total price of the items in the cart.
     *
     * @return string
     */
    public function total(): string
    {
        $content = $this->getContent();

        $total = $content->reduce(function ($total, $item) {
            return $total += $item->get('price') * $item->get('quantity');
        });

        return number_format($total, 2);
    }

    /**
     * Returns the content of the cart.
     *
     * @return Illuminate\Support\Collection
     */
    protected function getContent(): Collection
    {
        return $this->session->has(self::DEFAULT_INSTANCE) ? $this->session->get(self::DEFAULT_INSTANCE) : collect([]);
    }

    /**
     * Creates a new cart item from given inputs.
     *
     * @param string $name
     * @param string $price
     * @param string $quantity
     * @param array $options
     * @return Illuminate\Support\Collection
     */
    protected function createCartItem(string $name, string $price, string $quantity, array $options): Collection
    {
        $price = floatval($price);
        $quantity = intval($quantity);

        if ($quantity < self::MINIMUM_QUANTITY) {
            $quantity = self::MINIMUM_QUANTITY;
        }

        return collect([
            'name' => $name,
            'price' => $price,
            'quantity' => $quantity,
            'options' => $options,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

正如我之前所说,服务类并非框架内置的,因此没有artisan make创建服务类的命令。您可以将服务类保存在任何您喜欢的地方。我将服务类保存在App/Services目录中。

CartService.php文件包含public和方法。名为、、公共protected方法分别负责将商品添加到购物车、从购物车中移除商品、更新购物车商品数量以及清空购物车。add()remove()update()clear()

其他公共方法是content()和分别total()负责返回购物车内容和添加商品的总价。

最后,受保护的方法getContent()负责createCartItem()在类方法中返回购物车内容并根据接收到的参数形成新的购物车项目。

现在服务类已经准备好了,您需要在控制器内部使用它。要在CartItemController.php文件中使用服务类,需要更新代码如下:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\CartService;
use App\Http\Requests\CartItemRequest;

class CartItemController extends Controller
{
    protected $cartService;

    /**
     * Instantiate a new controller instance.
     *
     * @return void
     */
    public function __construct(CartService $cartService)
    {
        $this->cartService = $cartService;
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $content = $this->cartService->content();
        $total = $this->cartService->total();

        return view('cart.index', [
            'content' => $content,
            'total' => $total,
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \App\Http\Requests\CartItemRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(CartItemRequest $request)
    {
        $this->cartService->add($request->id, $request->name, $request->price, $request->quantity, $request->options);

        return back()->with('success', 'Item added to cart');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $content = $this->cartService->content();

        $item = $content->get($id);

        return view('cart', compact('item'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $this->cartService->update($id, $request->id);

        return back()->with('success', 'Item updated in cart');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $this->cartService->remove($id);

        return back()->with('success', 'Item removed from cart');
    }
}
Enter fullscreen mode Exit fullscreen mode

由于零配置解析,服务容器会自动解析任何不依赖任何接口的类。因此,只需CartService在控制器构造函数中注入即可:

class CartItemController extends Controller
{
    protected $cartService;

    /**
     * Instantiate a new controller instance.
     *
     * @return void
     */
    public function __construct(CartService $cartService)
    {
        $this->cartService = $cartService;
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,该类的实例CartService已在控制器中可用,并可作为$this->cartService属性访问。控制器的其余操作已更新以使用该服务,如您所见,控制器现在变得更加简洁。

动作可重用性

除了使控制器更简洁之外,您还可以从任何地方访问购物车相关的操作。例如,考虑以下 LiveWire 组件:

<?php

namespace App\Http\Livewire;

use App\Facades\Cart;
use Livewire\Component;
use Illuminate\Contracts\View\View;

class ProductComponent extends Component
{
    public $product;
    public $quantity;

    /**
     * Mounts the component on the template.
     *
     * @return void
     */
    public function mount(): void
    {
        $this->quantity = 1;
    }

    /**
     * Renders the component on the browser.
     *
     * @return \Illuminate\Contracts\View\View
     */
    public function render(): View
    {
        return view('livewire.product');
    }

    /**
     * Adds an item to cart.
     *
     * @return void
     */
    public function addToCart(): void
    {
        Cart::add($this->product->id, $this->product->name, $this->product->unit_price, $this->quantity);
        $this->emit('productAddedToCart');
    }
}
Enter fullscreen mode Exit fullscreen mode

您可以从任何地方添加、移除、更新或清空购物车。在服务类实现之前,管理购物车的唯一方式是通过 HTTP 请求。现在,您甚至可以通过artisan命令来管理购物车。

结束语

本文讨论的服务类概念并非具体,我也不认为它是灵丹妙药。这只是我过去使用过的东西,而且没有任何问题。只要你谨慎使用这些类,不要过度使用,应该就可以了。

鏂囩珷鏉ユ簮锛�https://dev.to/fhsinchy/laravel-service-classes-explained-3m7p
PREV
我的编程之旅 - 编程战士
NEXT
我如何爱上终端