Reader Stacks

Laravel Controllers: Basic, Resource, and Cross-Controller Calls

Calling one controller's method from another is usually a sign the shared logic belongs in a service class instead — a controller resolving another controller through the container works, but it's rarely the cleanest fix.

Laravel Controllers: Basic, Resource, and Cross-Controller Calls

Beyond a basic controller, Laravel's resource controller convention and the (generally discouraged) pattern of calling one controller's method from another are both worth understanding — the latter especially for recognizing when it signals a design that should be refactored.

Generating a basic controller

php artisan make:controller ProductController
class ProductController extends Controller
{
    public function index()
    {
        $products = Product::paginate(10);
        return view('products.index', compact('products'));
    }

    public function show(Product $product)
    {
        return view('products.show', compact('product'));
    }
}

Generating a resource controller, with every CRUD method stubbed out

php artisan make:controller ProductController --resource

This generates index, create, store, show, edit, update, and destroy methods already stubbed out — matching Laravel's conventional set of seven RESTful actions for a resource, saving the effort of writing each method signature manually.

Registering all seven routes in one line

Route::resource('products', ProductController::class);
php artisan route:list --name=products

Route::resource() registers all seven conventional routes (matching HTTP verb, URL pattern, and controller method) in a single line — significantly more concise than defining each of the seven routes individually with separate Route::get()/post()/put()/delete() calls.

Registering only specific resource routes

Route::resource('products', ProductController::class)->only(['index', 'show']);

Route::resource('products', ProductController::class)->except(['destroy']);

Genuinely useful when a resource shouldn't expose the full set of seven actions — a public-facing product catalog, for instance, typically only needs index and show, with the create/edit/delete actions living instead behind a separate admin-only controller.

An API resource controller (excludes create/edit, which return HTML forms)

php artisan make:controller Api/ProductController --api
Route::apiResource('products', Api\ProductController::class);

--api and apiResource() both skip generating and registering the create and edit methods/routes — these two exist purely to return an HTML form for creating or editing a resource, which has no meaning for a pure JSON API that doesn't render Blade views at all.

Calling one controller's method from another

class OrderController extends Controller
{
    public function checkout(Request $request)
    {
        $productController = app(ProductController::class);
        $stockCheck = $productController->checkStock($request->productId);
        // ...
    }
}

Resolving another controller through the container (via app()) and calling one of its methods directly does technically work — but it's usually a signal that the shared logic (checkStock here) actually belongs in a dedicated service class instead, injectable independently into both controllers, rather than creating a direct dependency between two controllers that otherwise have no natural relationship.

The cleaner alternative: extracting shared logic into a service

class StockService
{
    public function checkStock(int $productId): bool
    {
        return Product::find($productId)?->stock > 0;
    }
}
class OrderController extends Controller
{
    public function checkout(Request $request, StockService $stockService)
    {
        $stockCheck = $stockService->checkStock($request->productId);
    }
}

Following the custom service class pattern covered elsewhere on this site, both ProductController and OrderController can independently inject StockService — this avoids the awkward, generally discouraged controller-to-controller dependency entirely, while still sharing the exact same underlying logic between them.