rosengate / sigil
Laravel-Exedra PHP 8 attributes based routing controller package
Requires
- rosengate/exedra: v1.0.14
Suggests
- spatie/laravel-fractal: 5.8.*
README
Laravel-Exedra PHP 8 attributes based routing controller package
Table of Contents
- Features
- Requirements
- Setup
- Basic Usages
- Routing Conventions
- Routing Attributes
- Sub-routing / Group
- Middlewares
- Meta Information
- DI Method Injection
- Utilities
- Console Commands
- Todos
- Drawbacks
- Feedbacks
- Why
- License
Features
- Couple your routing with the controller class
- PHP 8 attributes based routing component
- Nested routing
- Provide a flexible ways to control/design your application through means like :
- sub-routing based middleware
- meta information
- create your own attributes, and control it through your own middleware
Requirements
- Laravel
- PHP >= 8
- this packages overrides your laravel Http Kernel and wholly use routing/controller/middleware component from
rosengate/exedra
, however it still fallbacks to laravel routes when there's no matching routes.
Setup
1. Install package through composer
For Laravel 10 and below
composer require rosengate/sigil
For laravel 11 onwards
composer require psr/http-message ^1.1
composer require rosengate/sigil
2. Register Sigil\SigilProvider
For Laravel 10 and below
Register Sigil\Providers\SigilProvider
inside your config\app.php
/* * Package Service Providers... */ SigilProvider::class,
For Laravel 11 onwards, you can register this under bootstrap/providers.php
3. publish and cache the config
Run vendor:publish
php artisan vendor:publish --provider=Sigil\SigilProvider
Config cache
php artisan config:cache
4. Http Kernel Extension
Since this package interact changes at http level, sigil does it's own bridging through Laravel Http Kernel.
For laravel 10 and below, extend your App\Http\Kernel
with Sigil\SigilKernel
(as this package uses it's own routing and request dispatch).
<?php namespace App\Http; class Kernel extends \Sigil\SigilKernel { }
For Laravel 11 onwards, you may go to your bootstrap/app.php
and replace Application
with Sigil\SigilApplication
. This replacement handles Kernel extension on it's own.
Basic Usages
Provided with your installation is the root controller where you'd define your initial routing.
namespace App\Http\Controllers; class RootController extends \Sigil\Controller { public function groupWeb() { return WebController::class; } }
The second controller WebController
would be the front facing controller for your app (following laravel similar routing)
namespace App\Http\Controllers; use Exedra\Routeller\Attributes\Path; #[Path('/')] class WebController extends \Sigil\Controller { #[Path('/')] public function get() { return view('welcome'); } }
Routing Conventions
The routing registry through the controller is built upon conventions and prefix through method name.
Create an action only for particular (REST) methods
get()
,post()
,delete()
,patch()
,put()
- can also suffix with additional string for eg.
getUsers()
Examples
<?php namespace App\Http\Controllers; use Exedra\Routeller\Attributes\Path; #[Path('/')] class WebController extends \Sigil\Controller { #[Path('/contact-us')] public function getContactUs() { } #[Path('/contact-us')] public function postContactUs() { } }
Create an action for any methods
- prefix with
execute
WITH additional string- for eg.
executeContactUs()
- for eg.
Examples
<?php namespace App\Http\Controllers; use Exedra\Routeller\Attributes\Path; #[Path('/')] class WebController extends \Sigil\Controller { #[Path('/about-us')] public function executeAboutUs() { } }
Create a method based middleware
middleware
ormiddlewareAuth
Create a routing group
Nest a routing
- prefix with
group
WITH additional string- for eg.
groupBook
- for eg.
Routing setup
- if you prefer a more programmatically routing, you can create a
setup(Group $router)
method.
Routing Attributes
Path(string path)
define the path for the current route / routing group (relatively)Method(string method|array methods)
set method accessibilityName(string name)
set route nameTag(string tag)
tag the current routeMiddleware(string middlewareClass)
add middlewareState(string key, mixed value)
a mutable meta informationSeries(string key, mixed value)
an additive meta informationFlag(mixed flag)
an array of meta informationRequestable(bool)
set whether this route is requestable or notAsFailRoute
mark the selected route as fallback route
Sub-routing / Group
This package allows you nest your routing beneath another routing indefinitely. Your routing uri/path is relative as it goes down the depth.
Create a method with the name prefixed with group
, and return the name of the controller.
<?php use Exedra\Routeller\Attributes\Path; class RootController extends \Sigil\Controller { public function groupWeb() { return WebController::class; } public function groupAdmin() { return AdminController::class; } } #[Path('/admin')] class AdminController extends \Sigil\Controller { #[Path('/dashboard')] public function getDashboard() { } } #[Path('/')] class WebController extends \Sigil\Controller { public function groupEnquiries() { return EnquiriesController::class; } } #[Path('/enquiries')] class EnquiriesController extends \Sigil\Controller { #[Path('/form')] public function getForm() { } #[Path('/form')] public function postForm() { } }
The routing will give a result like :
GET /admin/dashboard
GET /enquries/form
POST /enquries/form
Middlewares
Feel free to use your laravel middlewares at it still follows the same signature, and the constructor arguments are also injected with laravel di container.
Global middlewares
If you follow the SigilSetup
above (by providing the array of middleware classes), you'll just need to maintain your list of middleware
in your App\Http\Kernel
$middleware
property.
Group/route based middlewares
A class based middlewares.
<?php namespace App\Http\Controllers; use Exedra\Routeller\Attributes\Path; use Exedra\Routeller\Attributes\Middleware; use App\Http\Middleware\VerifyCsrfToken; use Illuminate\Session\Middleware\StartSession; use App\Http\Middleware\EncryptCookies; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; #[Middleware(EncryptCookies::class)] #[Middleware(AddQueuedCookiesToResponse::class)] #[Middleware(StartSession::class)] class WebController extends \Sigil\Controller { #[Path('/contact-us')] #[Middleware(VerifyCsrfToken::class)] public function postContactUs() { } }
Method based middleware
You can make a middleware directly in the controller itself by prefixing the method name with middleware
.
While doing so, you can also inject any registered instance through the method arguments.
<?php namespace App\Http\Controllers; //.. imports #[Path('/blogs/:blog-id')] class BlogApiController extends \Sigil\Controller { public function middleware(Request $request, $next, BloggerModel $blogger) { $blog = BlogModel::findOrFail($request->route('blog-id')); app()->instance(BlogRepository::class, new BlogRepository($blogger, $blog)); return $next($request); } #[Path('/articles')] public function getArticles(BlogRepository $blogService) { return $blogService->getArticles(); } }
This method gives you more control over the context of the current routing through the use of middleware.
Meta Information
The nested nature of this framework allows us to design our app as flexible as we wish. However, there are three types of information we can use for this purpose.
State
A mutable key based information.
<?php //... use Sigil\Context; use Exedra\Routeller\Attributes\State; use Exedra\Routeller\Attributes\Path; #[Path('/')] #[State('is_ajax', true)] class WebController extends \Sigil\Controller { #[Path('/contact-us')] #[State('is_ajax', false)] public function getContactUs(Context $context) { var_dump($context->getState('is_ajax')); // false } }
Series
An additive / array based key specific information. New information is appended instead of mutated.
<?php use Sigil\Context; use Exedra\Routeller\Attributes\Path; use Exedra\Routeller\Attributes\Series; #[Series('roles', 'admin')] class AdminController extends \Sigil\Controller { #[Path('/dashboard')] #[Series('roles', 'librarian')] public function getLibrary(Context $context) { var_dump($context->getSeries('roles')); // prints ['admin', 'staff'] } #[Series('roles', 'accountant')] #[Path('/billing')] public function getOrders(Context $context) { var_dump($context->getSeries('roles')); // prints ['admin', 'accountant'] } }
Flag
An array of flags / information. Similiar to series, but more simpler.
<?php use Exedra\Routeller\Attributes\Flag; use Exedra\Routeller\Attributes\Path; use Sigil\Context; #[Flag('authenticated')] class AdminController extends \Sigil\Controller { #[Path('/dashboard')] #[Flag('is_beta')] public function getDashboard(Context $context) { var_dump($context->getFlags()); // ['authenticated', 'is_beta'] } }
Usages of Meta Information
Meta information are best used with a middleware where you could control the flow/behaviour/design of your application by your defined metas.
For eg, let's use some of the meta information we wrote above and write some pseudo codes.
use Sigil\Context; use App\Models\User; class RootController extends \Sigil\Controller { public function middleware($request, $next, Context $context) { if ($context->hasFlag('authenticated')) { // do some authentication if (!session()->has('user_id')) throw new NotAuthenticatedException(); $user = User::find(session()->get('user_id')); if ($context->hasFlag('is_beta')) { if (!$user->isBetaAllowed()) throw new NoAccessException(); } if ($context->hasSeries('roles')) if (!in_array($user->role, $context->getSeries('roles'))) throw new NoAccessException(); } return $next($request); } }
Make your own attributes
The simplest way to create your own attribute is by extending these meta information and use them on your own terms.
For eg. we want to have an attribute that determine which routing goes to which user roles.
<?php namespace App\Attributes; use Exedra\Routeller\Attributes\Series; #[\Attribute] class Role extends Series { public function __construct($role) { parent::__construct(static::class, $role); } }
Create a middleware to utilize this information.
<?php namespace App\Http\Middleware; use Sigil\Context; use \App\Attributes\Role; class RolesCheckMiddleware { public function handle($request, $next, Context $context) { if ($roles = $context->getSeries(Role::class)) { //.. do a check if user has these roles } return $next($request); } }
Then add this middleware in your App\Http\Kernel
Now you can use this attribute in any of your controller.
<?php namespace App\Http\Controllers; use Exedra\Routeller\Attributes\Path; use App\Attributes\Role; #[Path('/accounts')] #[Role('accountant')] class ManageAccountsController extends \Sigil\Controller { public function get() { } }
DI Method Injection
The DI wiring of this package make use of laravel container registry. So, anything that you registered on app() container can also be retrieved here.
For eg :
<?php use Exedra\Routeller\Attributes\Path; #[Path('/books/:book-id')] class BookApiController extends \Sigil\Controller { public function middleware($request, $next) { app()->instance(Book::class, Book::findOrFail($request->route('book-id'))); return $next($request); } #[Path('/')] public function get(Book $book) { return $book; } #[Path('/')] public function post(Book $book, $request) { $book->author = $request->author; $book->isbn = $request->isbn; $book->save(); return $book; } }
Utilities
Route-Model finder / registry
Example usages
<?php use Exedra\Routeller\Attributes\Path; use Sigil\Utilities\Attributes\Model; use App\Models\AuthorModel; #[Path('/authors/:author-id')] #[Model(AuthorModel::class, 'author-id')] class AuthorApiController extends \Sigil\Controller { public function get(AuthorModel $author) { return $author; } }
Handling exception
You can handle model not found exception by simply creating a middleware that catch such exception.
For eg.
<?php use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; class RootController extends \Sigil\Controller { public function middleware(Request $request, $next) { try { return $next($request); } catch (ModelNotFoundException $e) { //.. do something } } }
PHPLeague Transformer
PHP League Fractal transformer. Transform your api response from your laravel model/collection
by annotating your action with .
This package uses spatie/laravel-fractal
.
Usage
<?php use Exedra\Routeller\Attributes\Path; use Sigil\Utilities\Attributes\Model; use App\Models\OrderModels; use App\Transformers\OrderTransformer; #[Path('/orders/:order-id')] #[Model(OrderModel::class, 'order-id')] class OrderApiController extends \Sigil\Controller { #[Transform(OrderTransformer::class)] public function get(OrderModel $order) { return $order; } }
Renderer
Handle the content returns of your controller action by defining a renderer that implements Sigil\Contracts\Renderer
.
Setup
Console Commands
List routes
List all routes
php artisan sigil:routes
Filter routes under web.
routing
php artisan sigil:routes --name=web
Todos
laravel url generator compatibilitybetter installation / setup procedure- related artisan commands
route list- make controller
- Caching strategy / testing
- More stable release
Drawbacks
As this package completely use a different component for routing, in general it will be incompatible with any other packages
that make use of laravel routing or the
routes
folder. Also these components as of now :- Url Generator
- Redirection with route name
As of now, this package will still fallback to laravel routing when there're no matching routes.
Why
I wrote rosengate/exedra
back 4 years ago because i couldn't find a framework that can exactly do what I wanted, like hierarchically nest a routing beneath another routing.
Also exedra
was never meant to be another full-fledged framework. It's just a microframework and I always advocate for the use of tons of amazing php packages out there.
Then I built a phpdoc based routing controller component and since then writing a code with exedra
became a bliss than ever.
But building things from microframework can be daunting as I always needed an ORM, validation, error handling, and many other tools out there (I always find myself using Elqouent).
Then at one point I became so used with laravel and decided to try it with exedra. I was starting to think that this is kinda possible. Then PHP8 came with a news so good I've been waiting for years. Attributes/Annotation. So I decided to just port it for laravel and see how it goes here. <3
Feedbacks
- Feel free to throw in feedbacks through github issues.
- I am planning to find a way to integrate withIlluminate\Contracts\Routing\UrlGenerator
soon.