poshtive/router
Convention-based route discovery for Laravel controllers. Public methods become routes automatically, with PHP attributes for HTTP verb overrides, middleware, where constraints, prefixes, and model-bound parameters. Supports Laravel 12/13 and PHP 8.3.
Install and verify compatibility first.
composer require poshtive/router
php artisan vendor:publish --provider="Poshtive\Router\RouterServiceProvider" --tag="config"
Before anything else, confirm the declared requirements against your app: PHP ^8.3 and Laravel components ^13.0. This is the single biggest adoption gate — if you're on Laravel 10/11/12, stop here. Also reconcile the naming: the README badges and links point at qoqn/laravel-router while the install target is poshtive/router. Confirm the Packagist package name with composer show poshtive/router and skim vendor/poshtive/router/src before trusting the docs.
Where to look first — in this order:
config/router.php — the five knobs (convention, method_extends, http_methods_map, report_skipped_routes, strict) decide almost everything about your daily experience.Poshtive\Router\Attributes\* — Route, Where, LocalOnly, DoNotDiscover, IgnoreParentMiddleware. These are your escape hatches.Poshtive\Router\Router::discover() — the only public entry point you'll call.First use case — a scoped pilot, not a migration. Don't point discovery at your whole app/Http/Controllers on day one. Add one line to routes/web.php, targeting a single small subdirectory:
use Poshtive\Router\Router;
Router::create()->discover(app_path('Http/Controllers/Marketing'));
Then immediately run php artisan route:list --path=marketing and compare against what you expected. Start with a fresh, conventionally-shaped controller set (an IndexController plus one resource controller) rather than retrofitting existing controllers — you'll learn the path-construction rules in ten minutes instead of debugging them for a week.
Pick one convention and enforce it repo-wide. Mixing prefix and attribute_or_get across teams produces routes that are impossible to grep for. prefix (getIndex, postStore, deleteDestroyApp) is self-documenting with zero attributes and is the better default for greenfield. attribute_or_get is the better fit for teams already writing clean REST method names — pair it with http_methods_map so store/update/destroy don't need a #[Route(method: ...)] on every method.
Layered middleware via abstract bases. The combination that actually pays off day-to-day:
#[DoNotDiscover]
#[Route(middleware: ['auth', 'verified'])]
abstract class AuthenticatedController extends Controller {}
class UserController extends AuthenticatedController {
public function index() {} // GET /user, with auth+verified
#[IgnoreParentMiddleware]
public function show() {} // GET /user/show, no inherited middleware
}
Combine with method_extends => true for shared CRUD bases. Note the asymmetry: #[IgnoreParentMiddleware] on a class drops parent middleware but keeps the class's own; on a method it drops everything class-level.
Nested resources via child-controller folders. A User/ directory next to UserController produces /user/{user}/profile/.... This is the package's most opinionated feature — the rule is that every child controller method must take at least one Eloquent model parameter or registration fails. Adopt it only if your nested resources genuinely hang off a parent model.
Keep manual routes for non-conventional things. Webhooks, third-party callbacks, signed URLs, and anything with a fixed external path stay in routes/web.php or routes/api.php. There's nothing wrong with a hybrid file — the package explicitly supports it. Register discovered routes before any catch-all or wildcard routes so they aren't shadowed.
Constraints go on the method, not the parameter. #[Where('id', '\d+')] stacked multiple times; all constraints must pass. Use this to keep /user/{id}/show from swallowing /user/create in route matching order.
CI pattern: turn on strict => true and report_skipped_routes => true in your testing env, so duplicate registrations fail the build and silently-dropped controllers show up in logs. Keep strict => false locally if you want leniency during refactors.
private/protected, or the whole class needs #[DoNotDiscover].strict => false, collisions are only logged — and only if a logger is available. Two controllers mapping to the same VERB + URI will shadow each other with no visible error. Turn strict on before you have more than a handful of controllers.update(int $id, string $section) becomes /user/{id}/update/{section} — the model param jumps before the action name. Either adopt that shape consistently or reach for keepOrder: true, but don't mix.method_extends defaults to false. Trait and parent-class methods are invisible until you flip it, and flipping it can double-register if the parent isn't marked #[DoNotDiscover]. Audit before enabling.#[Route] only honors middleware. uri, method, and keepOrder on the class are ignored — a common wasted half-hour.Request, services, and value objects are dropped from the signature. That's usually what you want, but it means a method with only non-model params produces a route with no parameters at all.#[LocalOnly] plus route caching is a trap. Run php artisan route:cache in a local environment and those dev-only routes get baked into the cached manifest. Always cache in the target environment, and add a deploy check that local-only paths 404 in staging/production.php artisan route:cache actually works with your setup and that route registration is deterministic (1.0.0 claims it is for tied priorities) — otherwise you're paying a directory walk on every request.php artisan route:list --path=<segment> -v first, then a one-off tinker snippet instantiating Router::create() and dumping the discovered class list. There's no documented post-discovery event or naming hook, so if you need custom URI generation beyond the attributes, plan on a thin wrapper class rather than forking.src/ before you depend on it, and keep your manual routes as the fallback path so extraction is cheap.How can I help you explore Laravel packages today?