Skip to main content
Version: 5.x

Controllers

Introduction​

Controllers organize request-handling logic into dedicated classes, instead of defining closures in route files. Controllers are stored in app/Controllers/.

Bow supports two declaration styles:

  • Routes defined in routes/app.php β€” routes point to a controller using the Controller::action syntax (the style described throughout most of this document).
  • PHP 8 attribute routing β€” routes are declared directly on the class with #[Controller], #[Get], #[Post], etc. (see Attribute routing).

Creating a controller​

php bow add:controller UserController

Simple controller​

app/Controllers/UserController.php
namespace App\Controllers;

use App\Models\User;
use Bow\Http\Request;

class UserController
{
public function index()
{
$users = User::all();

return view('users/index', compact('users'));
}

public function show(Request $request, int $id)
{
$user = User::retrieve($id);

return view('users/show', compact('user'));
}

public function store(Request $request)
{
$user = User::create([
'name' => $request->get('name'),
'email' => $request->get('email'),
]);

return redirect('/users/' . $user->id);
}
}

Defining the routes​

routes/app.php
$app->get('/users', 'UserController::index');
$app->get('/users/:id', 'UserController::show');
$app->post('/users', 'UserController::store');

Dependency injection​

Dependencies are automatically injected into constructors and methods:

app/Controllers/OrderController.php
namespace App\Controllers;

use App\Services\PaymentService;
use App\Services\NotificationService;
use Bow\Http\Request;

class OrderController
{
public function __construct(
private PaymentService $paymentService,
private NotificationService $notificationService
) {
}

public function store(Request $request)
{
$order = $this->paymentService->processOrder(
$request->only(['product_id', 'quantity'])
);

$this->notificationService->sendOrderConfirmation($order);

return response()->json($order, 201);
}
}

Invokable controller​

For single-action controllers, use __invoke:

app/Controllers/ShowDashboardController.php
namespace App\Controllers;

use App\Services\DashboardService;
use Bow\Http\Request;

class ShowDashboardController
{
public function __construct(
private DashboardService $dashboard
) {
}

public function __invoke(Request $request)
{
return view('dashboard', [
'stats' => $this->dashboard->getStats(),
]);
}
}
routes/app.php
$app->get('/dashboard', ShowDashboardController::class);

Namespaces​

For nested controllers, use the relative path:

// Controller: App\Controllers\Admin\UserController
$app->get('/admin/users', 'Admin\UserController::index');

Generate a nested controller:

php bow add:controller Admin/UserController

Middleware​

Apply middleware to routes:

$app->get('/profile', 'ProfileController::show')->middleware('auth');

// Multiple middleware
$app->get('/admin', 'AdminController::index')->middleware(['auth', 'admin']);

REST controller​

REST controllers make it easy to build RESTful APIs.

Generating a REST controller​

php bow generate:resource ArticleController

Generated structure​

app/Controllers/ArticleController.php
namespace App\Controllers;

use Bow\Http\Request;

class ArticleController
{
/**
* GET /articles
*/
public function index(): void
{
// List of articles
}

/**
* POST /articles
*/
public function store(Request $request): void
{
// Create an article
}

/**
* GET /articles/:id
*/
public function show(Request $request, mixed $id): void
{
// Display an article
}

/**
* PUT /articles/:id
*/
public function update(Request $request, mixed $id): void
{
// Update an article
}

/**
* DELETE /articles/:id
*/
public function destroy(Request $request, mixed $id): void
{
// Delete an article
}
}

Registering REST routes​

routes/app.php
$app->rest('articles', 'ArticleController');

Generated routes​

URLMethodActionName
/articlesGETindexarticles.index
/articlesPOSTstorearticles.store
/articles/:idGETshowarticles.show
/articles/:idPUTupdatearticles.update
/articles/:idDELETEdestroyarticles.destroy
PUT only (no PATCH)

The update method is registered for PUT only. If you also want to expose PATCH (partial update), add a dedicated route:

$app->patch('/articles/:id', 'ArticleController::update')
->where('id', '\d+');
Route names and nested URLs

The name is built from str_replace('/', '.', $url) . '.' . $action. With a URL that has a leading slash such as '/api/v1/articles', you will get names like .api.v1.articles.index (leading dot). Pass the URL without a leading slash ('api/v1/articles') for clean names.

Parameter constraints​

// Global constraint
$app->rest('articles', 'ArticleController', ['id' => '\d+']);

// Per-method constraints
$app->rest('articles', 'ArticleController', [
'show' => ['id' => '\d+'],
'update' => ['id' => '\d+'],
]);

Ignoring methods​

$app->rest('articles', [
'controller' => 'ArticleController',
'ignores' => ['destroy'],
], ['id' => '\d+']);

Controllers with attributes​

To avoid maintaining a central route file, you can declare routes directly on the class using PHP 8 attributes:

app/Controllers/ArticleController.php
namespace App\Controllers;

use Bow\Http\Request;
use Bow\Router\Attributes\{Controller, Get, Post, Put, Delete};

#[Controller(prefix: '/articles', middleware: ['auth'], name: 'articles.')]
final class ArticleController
{
#[Get('/', name: 'index')]
public function index() { /* ... */ }

#[Get('/:id', name: 'show', where: ['id' => '\d+'])]
public function show(Request $request, int $id) { /* ... */ }

#[Post('/', name: 'store', middleware: ['validate'])]
public function store(Request $request) { /* ... */ }

#[Put('/:id', name: 'update')]
public function update(Request $request, int $id) { /* ... */ }

#[Delete('/:id', name: 'destroy')]
public function destroy(Request $request, int $id) { /* ... */ }
}

Then all you need to do is register the class in the route file:

routes/app.php
$app->register(\App\Controllers\ArticleController::class);

// Or a batch of controllers
$app->register([
\App\Controllers\ArticleController::class,
\App\Controllers\CommentController::class,
]);

The name: prefix of #[Controller] is concatenated verbatim to the name of each route β€” choose 'articles.' (with a dot) to get articles.index, articles.show, etc. See Routing > Attribute routing for the full list of attributes (#[Patch], #[Options], #[Route]…).

Namespace configuration​

Customize component namespaces in app/Kernel.php:

app/Kernel.php
public function namespaces(): array
{
return [
'controller' => 'App\\Controllers',
'middleware' => 'App\\Middlewares',
'listener' => 'App\\Listeners',
];
}
KeyUsed by
controllerResolving 'UserController::action' β†’ App\Controllers\UserController, php bow add:controller, generate:resource
middlewarephp bow add:middleware
listenerphp bow add:listener

Is something missing?

If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.