I have a site which is localized into several languages. Every public route is prefixed with the locale key (e.g. /{locale}/foo/bar), which gets caught and applied by middleware.
When generating URLs to point to other pages, I end up needing to feed the current locale into every url, like so:
Foo Bar
Otherwise the output url will contain %7Blocale%7D, which breaks it. This strikes me as needlessly verbose. Is there not a way to specify a default value for a given named parameter, such that if no value is explicitly provided for 'locale' it can be defaulted to whatever the current locale is?
I've inspected the UrlGenerator class, but I don't see anything to that effect.
The Route class has a defaults property, but that only appears to be used as part of binding the route to the current request.
Ultimately, not a huge issue, just wondering if anyone has any ideas for ways to save a bit of sanity.
You can use URL defaults as well at a middleware:
use Illuminate\Support\Facades\URL;
URL::defaults(
[
'locale' => $locale
]
);
When you define your routes, use optional variables with defaults:
Routes:
Route::get('{locale?}/foo/bar', 'Controller#fooBar');
Controller:
public function __construct()
{
$this->locale = session()->has('locale') ? session('locale') : 'en';
}
public function fooBar($locale = null)
{
$locale = $locale ?: $this->locale;
}
OR:
public function fooBar($locale = 'en')
{
$locale = $locale ?: $this->locale;
}
Wherever you call your route:
Foo Bar
You could optionally put the constructor in a BaseController class that all your other controllers extend.
There may be better ways to do this, but this would keep you from having to include the locale wherever you call a route.
There isn't any built in means of doing this, but I managed to achieve the desired result by extending the UrlGenerator
<?php
namespace App\Services;
use Illuminate\Routing\UrlGenerator as BaseGenerator;
use Illuminate\Support\Arr;
class UrlGenerator extends BaseGenerator
{
protected $default_parameters = [];
public function setDefaultParameter($key, $value){
$this->default_parameters[$key] = $value;
}
public function removeDefaultParameter($key){
unset($this->default_parameters[$key]);
}
public function getDefaultParameter($key){
return isset($this->default_parameters[$key]) ? $this->default_parameters[$key] : null;
}
protected function replaceRouteParameters($path, array &$parameters)
{
if (count($parameters) || count($this->default_parameters)) {
$path = preg_replace_sub(
'/\{.*?\}/', $parameters, $this->replaceNamedParameters($path, $parameters)
);
}
return trim(preg_replace('/\{.*?\?\}/', '', $path), '/');
}
protected function replaceNamedParameters($path, &$parameters)
{
return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) {
return isset($parameters[$m[1]]) ? Arr::pull($parameters, $m[1]) : ($this->getDefaultParameter($m[1]) ?: $m[0]);
}, $path);
}
}
Then rebinding our subclass into the service container
class RouteServiceProvider extends ServiceProvider
{
public function register(){
parent::register();
//bind our own UrlGenerator
$this->app['url'] = $this->app->share(function ($app) {
$routes = $app['router']->getRoutes();
$url = new UrlGenerator(
$routes, $app->rebinding(
'request', function ($app, $request) {
$app['url']->setRequest($request);
}
)
);
$url->setSessionResolver(function () {
return $this->app['session'];
});
$app->rebinding('routes', function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
}
//...
}
Then all I needed to do was inject the default locale into the UrlGenerator from the Locale middleware
public function handle($request, Closure $next, $locale = null) {
//...
$this->app['url']->setDefaultParameter('locale', $locale);
return $next($request);
}
Now route('foo.bar') will automatically bind the current locale to the route, unless another is explicitly provided.
Related
Apologies for the title, I wasn't sure of the best way to phrase this.
Essentially I'm trying replicate the absolute basics of Laravel's routing in a small personal application.
Below is the API I'm aiming for, however, I'm not sure how to access the namespace (api/v1) that's defined in the original Route::namespace call.
Route::namespace('api/v1')->group(function() {
Route::get('/posts/{id}', [PostController::class, 'show']);
Route::post('/posts', [PostController::class, 'store']);
});
So within the Route::get and Route::post methods, I need to somehow access that namespace value that was set via the outer call to Route::namespace. So Route::post('/posts') will end up registering an endpoint with the URI api/v1/posts.
Any pointers or guidance would much appreciated, Thanks.
Also here's the basic relevant parts of the Route class for reference.
class Route {
public function __construct(public string $namespace = '', private string $uri = '', private mixed $handle = null, private string $type = '')
{
return $this;
}
public static function namespace(string $namespace): Route
{
return new Route(namespace: $namespace);
}
public function group(Closure $callback): self
{
$callback($this);
return $this;
}
public static function get(string $uri, mixed $handle): Route
{
return new Route(uri: $uri, handle: $handle, type: 'get');
}
public static function post(string $uri, mixed $handle): Route
{
return new Route(uri: $uri, handle: $handle, type: 'post');
}
}
I have the following routes in routes/api.php:
Route::get('items/{item}', function(Guid $item) {...});
Route::get('users/{user}', function(Guid $user) {...});
Since Guid is a custom type, how can I resolve that via dependency injection? As shown, the route parameter {item} differs from the callback parameter type-hint:Guid so it can not be automatically resolved.
That's what I've tried in app/Providers/AppServiceProvider.php:
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->app->bind(Guid::class, function(Application $app, array $params) {
return Guid::fromString($params[0]);
});
}
}
I'd expect $params to be something like this: [ 'item' => 'guid' ] -- but it is: [].
You can make use of explicit binding Laravel Routing:
in RouteServiceProvider::boot():
public function boot()
{
Route::model('item', Guid $item);
Route::model('user', Guid $user);
}
If Guid is not a model use a Closure to map onto the string:
Route::bind('user', function ($value) {
return Guid::fromString($value);
});
UPDATED
And I found another way, much better - implement UrlRoutable contract Lavaravel API:
<?php
namespace App\Models;
use Illuminate\Contracts\Routing\UrlRoutable;
class Guid implements UrlRoutable
{
private string $guid;
public function setGuid(string $guid)
{
$this->guid = $guid;
return $this;
}
public function getGuid(): string
{
return $this->guid;
}
public static function fromString(string $guid): self
{
//you cannot set props from constructor in this case
//because binder make new object of this class
//or you can resolve constructor depts with "give" construction in ServiceProvider
return (new self)->setGuid($guid);
}
public function getRouteKey()
{
return $this->guid;
}
public function getRouteKeyName()
{
return 'guid';
}
public function resolveRouteBinding($value, $field = null)
{
//for using another "fields" check documentation
//and maybe another resolving logic
return self::fromString($value);
}
public function resolveChildRouteBinding($childType, $value, $field)
{
//or maybe you have relations
return null;
}
}
And, with this, you can use routes like you want as Guid now implements UrlRoutable and can turn {item} (or whatever) URL-path sub-string markers into Guids per dependency injection (by the type-hint as you asked for it):
Route::get('items/{item}', function(Guid $item) {
return $item->getGuid();
});
BTW: NEVER EVER use closures in routes as you cannot cache closure routes - and routes are good to be optimized, and caching helps with that in Laravel routing.
simple helper to utilize route binding callback.
if (!function_exists('resolve_bind')) {
function resolve_bind(string $key, mixed $value) {
return call_user_func(Route::getBindingCallback($key), $value);
}
}
usage
resolve_bind('key', 'value');
I am having an issue setting up an injection on both the constructor and the method in a controller.
What I need to achieve is to be able to set up a global controller variable without injecting the same on the controller method.
From below route;
Route::group(['prefix' => 'test/{five}'], function(){
Route::get('/index/{admin}', 'TestController#index');
});
I want the five to be received by the constructor while the admin to be available to the method.
Below is my controller;
class TestController extends Controller
{
private $five;
public function __construct(PrimaryFive $five, Request $request)
{
$this->five = $five;
}
public function index(Admin $admin, Request $request)
{
dd($request->segments(), $admin);
return 'We are here: ';
}
...
When I run the above, which I'm looking into using, I get an error on the index method:
Symfony\Component\Debug\Exception\FatalThrowableError thrown with message "Argument 1 passed to App\Http\Controllers\TestController::index() must be an instance of App\Models\Admin, string given"
Below works, but I don't need the PrimaryFive injection at the method.
class TestController extends Controller
{
private $five;
public function __construct(PrimaryFive $five, Request $request)
{
$this->five = $five;
}
public function index(PrimaryFive $five, Admin $admin, Request $request)
{
dd($request->segments(), $five, $admin);
return 'We are here: ';
}
...
Is there a way I can set the constructor injection with a model (which works) and set the method injection as well without having to inject the model set in the constructor?
One way you could do this is to use controller middleware:
public function __construct()
{
$this->middleware(function (Request $request, $next) {
$this->five = PrimaryFive::findOrFail($request->route('five'));
$request->route()->forgetParameter('five');
return $next($request);
});
}
The above is assuming that PrimaryFive is an Eloquent model.
This will mean that $this->five is set for the controller, however, since we're using forgetParameter() it will no longer be passed to your controller methods.
If you've specific used Route::model() or Route::bind() to resolve your five segment then you can retrieve the instance straight from $request->route('five') i.e.:
$this->five = $request->route('five');
The error is because of you cannot pass a model through the route. it should be somethiing like /index/abc or /index/123.
you can use your index function as below
public function index($admin,Request $request){}
This will surely help you.
Route::group(['prefix' => 'test/{five}'], function () {
Route::get('/index/{admin}', function ($five, $admin) {
$app = app();
$ctr = $app->make('\App\Http\Controllers\TestController');
return $ctr->callAction("index", [$admin]);
});
});
Another way to call controller from the route. You can control what do you want to pass from route to controller
App::bind('App\Http\Repositories\languageRepository',
function( $app, array $parameters)
{
return new App\Http\Repositories\languageRepository($parameters[0]);
} );
Route::get('/test/{id}', 'testController#getme');
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Repositories\languageRepository;
class test extends Controller
{
//
protected $language;
public function __construct(languageRepository $rep){
$this->language = $rep;
}
public function getme(){
$this->language->getMe();
}
}
When user accesses the route /test/5 for example, it goes to test Controller. what I'd like to do is that it should automatically pass my route parameter to App:bind function and automatically create languageRepository class with the constructor value passed as my route paramter. what happens is the code actually tells me $parameters[0] is undefined offset. why is that? I've tried App::make but then how do I pass the parameter from route to App::make?
You can accomplish this using the container's request instance, for query parameters:
App::bind('App\Http\Repositories\languageRepository',function($app)
{
$request = $app['request'];
$parameters = $request->all();
return new App\Http\Repositories\languageRepository($parameters[0]);
});
You can accomplish this using the container's request instance, for a route parameter:
App::bind('App\Http\Repositories\languageRepository',function($app)
{
$request = $app['request'];
$segment = $request->segment(1);
return new App\Http\Repositories\languageRepository($segment);
});
I know that I can generate URL passing the route name
<?php echo $this->url('route-name') #in view file ?>
But can I get information in opposite direction?
From current URL/URI, I need to get route name.
Real case is: I have layout.phtml where is the top menu (html).
Current link in the menu need to be marked with css class. So, example what I need is:
<?php // in layout.phtml file
$index_css = $this->getRouteName() == 'home-page' ? 'active' : 'none';
$about_css = $this->getRouteName() == 'about' ? 'active' : 'none';
$contact_css = $this->getRouteName() == 'contact' ? 'active' : 'none';
?>
I am using fast route, but I am interesting in any solution. Solution doesn't have to be in View file.
From my research, there is such information in RouteResult instance in the public method getMatchedRouteName(). The problem is how to reach to this instance from the View.
We know that we can get RouteResult, but from the Request object which is in a Middleware's __invoke() method.
public function __invoke($request, $response, $next){
# instance of RouteResult
$routeResult = $request->getAttribute('Zend\Expressive\Router\RouteResult');
$routeName = $routeResult->getMatchedRouteName();
// ...
}
As #timdev recommended we will find inspiration in existing helper UrlHelper and make almost the same implementation in custom View Helper.
In short we will create 2 classes.
CurrentUrlHelper with method setRouteResult() and
CurrentUrlMiddleware with __invoke($req, $res, $next)
We will inject the CurrentUrlHelper in CurrentUrlMiddleware and
in __invoke() method call the CurrentUrlHelper::setRouteResult() with appropriate RouteResult instance.
Later we can use our CurrentUrlHelper with RouteResult instance in it. Both classes should have an Factory too.
class CurrentUrlMiddlewareFactory {
public function __invoke(ContainerInterface $container) {
return new CurrentUrlMiddleware(
$container->get(CurrentUrlHelper::class)
);
}
}
class CurrentUrlMiddleware {
private $currentUrlHelper;
public function __construct(CurrentUrlHelper $currentUrlHelper) {
$this->currentUrlHelper = $currentUrlHelper;
}
public function __invoke($request, $response, $next = null) {
$result = $request->getAttribute('Zend\Expressive\Router\RouteResult');
$this->currentUrlHelper->setRouteResult($result);
return $next($request, $response); # continue with execution
}
}
And our new helper:
class CurrentUrlHelper {
private $routeResult;
public function __invoke($name) {
return $this->routeResult->getMatchedRouteName() === $name;
}
public function setRouteResult(RouteResult $result) {
$this->routeResult = $result;
}
}
class CurrentUrlHelperFactory{
public function __invoke(ContainerInterface $container){
# pull out CurrentUrlHelper from container!
return $container->get(CurrentUrlHelper::class);
}
}
Now we only need to register our new View Helper and Middleware in the configs:
dependencies.global.php
'dependencies' => [
'invokables' => [
# dont have any constructor!
CurrentUrlHelper::class => CurrentUrlHelper::class,
],
]
middleware-pipeline.global.php
'factories' => [
CurrentUrlMiddleware::class => CurrentUrlMiddlewareFactory::class,
],
'middleware' => [
Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE,
Zend\Expressive\Helper\UrlHelperMiddleware::class,
CurrentUrlMiddleware::class, # Our new Middleware
Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE,
],
And finaly we can register our View Helper in templates.global.php
'view_helpers' => [
'factories' => [
# use factory to grab an instance of CurrentUrlHelper
'currentRoute' => CurrentUrlHelperFactory::class
]
],
it's important to register our middleware after ROUTING_MIDDLEWARE and before DISPATCH_MIDDLEWARE!
Also, we have CurrentUrlHelperFactory only to assign it to the key 'currentRoute'.
Now you can use helper in any template file :)
<?php // in layout.phtml file
$index_css = $this->currentRoute('home-page') ? 'active' : 'none';
$about_css = $this->currentRoute('about') ? 'active' : 'none';
$contact_css = $this->currentRoute('contact') ? 'active' : 'none';
?>
As you note in your self-answer, UrlHelper is a useful thing to notice. However, creating a new helper that depends on UrlHelper (and reflection) isn't ideal.
You're better off writing your own helper, inspired UrlHelper but not dependent on it.
You can look at the code for UrlHelper, UrlHelperFactory and UrlHelperMiddleware to inform your own implementation.
You could wrap the template renderer in another class and pass the Request to there, subtract what you need and inject it into the real template renderer.
Action middleware:
class Dashboard implements MiddlewareInterface
{
private $responseRenderer;
public function __construct(ResponseRenderer $responseRenderer)
{
$this->responseRenderer = $responseRenderer;
}
public function __invoke(Request $request, Response $response, callable $out = null) : Response
{
return $this->responseRenderer->render($request, $response, 'common::dashboard');
}
}
The new wrapper class:
<?php
declare(strict_types = 1);
namespace Infrastructure\View;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Diactoros\Stream;
use Zend\Expressive\Router\RouteResult;
use Zend\Expressive\Template\TemplateRendererInterface;
class ResponseRenderer
{
private $templateRenderer;
public function __construct(TemplateRendererInterface $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function render(Request $request, Response $response, string $templateName, array $data = []) : Response
{
$routeResult = $request->getAttribute(RouteResult::class);
$data['routeName'] = $routeResult->getMatchedRouteName();
$body = new Stream('php://temp', 'wb+');
$body->write($this->templateRenderer->render($templateName, $data));
$body->rewind();
return $response->withBody($body);
}
}
Code is borrowed from GitHub.