Laravel 5.3 SubstituteBindings middleware with withoutMiddleware issue - laravel

Since Laravel 5.3, the route implicit binding works as middleware called SubstituteBindings. I used to work with Laravel 5.2 and upgraded to 5.3.
I have some custom middlewares in my application and in my tests I need to disable them. So, until now I used $this->withoutMiddleware() in the test methods. But since the update to Laravel 5.3, withoutMiddleware stops the route implicit binding, and all my tests fails.
I don't know if this should be considered as bug, but it is a huge problem for me.
Is there any way to set the SubstituteBindings middleware as mandatory middleware? How can I still use implicit binding and test my tests without other middlewares?

Building on my comment above I had a look at registering a custom router which always adds SubstituteBindings to the list of middleware if middleware was disabled. You can achieve it by registering a custom RoutingServiceProvider and registering your own Router class. Unfortunately since the route is created fairly early on in the app bootstrap process you also need to create a custom App class and use that in bootstrap/app.php too.
RoutingServiceProvider
<?php namespace App\Extensions\Providers;
use Illuminate\Routing\RoutingServiceProvider as IlluminateRoutingServiceProvider;
use App\Extensions\ExtendedRouter;
class RoutingServiceProvider extends IlluminateRoutingServiceProvider
{
protected function registerRouter()
{
$this->app['router'] = $this->app->share(function ($app) {
return new ExtendedRouter($app['events'], $app);
});
}
}
Custom router
This adds the middleware, it just extends the default router but overrides the runRouteWithinStack method and, instead of returning an empty array if $this->container->make('middleware.disable') is true, it returns an array containing the SubstituteBindings class.
<?php namespace App\Extensions;
use Illuminate\Routing\Router;
use Illuminate\Routing\Route;
use Illuminate\Routing\Pipeline;
use Illuminate\Http\Request;
class ExtendedRouter extends Router {
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
// Make sure SubstituteBindings is always used as middleware
$middleware = $shouldSkipMiddleware ? [
\Illuminate\Routing\Middleware\SubstituteBindings::class
] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run($request)
);
});
}
}
Custom App Class
<?php namespace App;
use App\Extensions\Providers\RoutingServiceProvider;
class MyCustomApp extends Application
{
protected function registerBaseServiceProviders()
{
parent::registerBaseServiceProviders();
$this->register(new RoutingServiceProvider($this));
}
Using the custom app class
In bootstrap/app.php change the line where the app is instantiated to:
$app = new App\MyCustomApp(
realpath(__DIR__.'/../')
);
--
Warning! I haven't fully tested this, my app loads and my tests pass but there could be issues that I haven't discovered. It's also quite brittle since if the Laravel base Router class changes you might find things break randomly on future upgrades.
--
You might also want to refactor this so the list of middleware in the custom router always contains the SubstituteBindings class so there isn't so much of a difference in behaviour if middleware is disabled.

Related

Unidentified Controller when using a route that calling a controller

Good day, and thank you for reading this problem
I have a problem where I'm using a different parameter but it doesn't work, here's the problem code
Route::get('/profiles','ProfilesController#index');
But when I'm using this code it worked perfectly fine
Route::get('/profiles',[ProfilesController::class, 'index']);
Here's the controller
class ProfilesController extends Controller
{
public function index()
{
return profiles::all();
}
}
You need to use full namespace App\Http\Controllers\ProfilesController#index
use App\Http\Controllers\ProfilesController;
// Using PHP callable syntax...
Route::get('/profiles', [ProfilesController::class, 'index']);
// Using string syntax...
Route::get('/profiles', 'App\Http\Controllers\ProfilesController#index');
If you would like to continue using the original auto-prefixed controller routing, you can simply set the value of the $namespace property within your RouteServiceProvider and update the route registrations within the boot method to use the $namespace property.
More info:
https://laravel.com/docs/8.x/upgrade#automatic-controller-namespace-prefixing

Laravel installation in sub-folder and horizon not working

I have installed the Laravel in sub-folder and is trying to install the horizon. After routing to "test.com/sub-folder/horizon", all the design in broken and also the internal links are pointing to main domain instead of main-domain-without-subfolder.
After the search, it seems to be the known issue which is already reported in github issue
Has there is any work around to make horizon work when Laravel is installed in sub-folder?
I have a solution that only involves PHP.
The issue, as pointed out by #Isaiahiroko, is the basePath defined for Horizon's interface. That code is in Laravel\Horizon\Http\Controllers\HomeController::index(). The idea is this: we are going to pass to Laravel's service container our own implementation of that controller that will override the basePath definition passed to Horizon's interface.
Create a new controller with code like this:
<?php
namespace App\Http\Controllers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\Http\Controllers\HomeController;
class HorizonHomeController extends HomeController
{
/**
* Overrides default horizon route to support subdirectory hosting.
*/
public function index ()
{
// We use a plain request to check for the base url.
$request = request();
// Set up our base path.
$base_path = Str::substr($request->getBasePath(), 1);
if (!empty($base_path)) {
$base_path .= '/';
}
// Patch default horizon variables with our own base path.
$variables = Horizon::scriptVariables();
$variables['path'] = $base_path . config('horizon.path');
// Render horizon's home view.
return view('horizon::layout', [
'assetsAreCurrent' => Horizon::assetsAreCurrent(),
'horizonScriptVariables' => $variables,
'cssFile' => Horizon::$useDarkTheme ? 'app-dark.css' : 'app.css',
'isDownForMaintenance' => App::isDownForMaintenance(),
]);
}
}
What's left is telling Laravel's service container that when Horizon's HomeController is requested, it should provide our HorizonHomeController class. In your AppServiceProvider, at the end of the register() method, set this up:
// [...]
class AppServiceProvider extends ServiceProvider
{
// [...]
/**
* Register any application services.
*
* #return void
* #throws InvalidConfiguration
*/
public function register()
{
// [...]
// Horizon's subdirectory hack
$this->app->bind(
Laravel\Horizon\Http\Controllers\HomeController::class,
App\Http\Controllers\HorizonHomeController::class
);
}
// [...]
}
After that, you should be able to browse to http(s)://<your-host>/<your-sub-dir>/horizon normally.
Considerations:
To me this feels cleaner that patching a compiled js, which also has the downside that needs to be re-applied every time Horizon is updated (this can be mitigated with a post-update script in composer, tho). Also, for additional points, this solution is only overriding the method that renders the view, but not the route, which means all of Horizon's authentication mechanisms (middlewares and gates) are working exactly as described in the documentation.
If you desperately need to do this, here is a hack:
In public\vendor\horizon\app.js, search for window.Horizon.basePath
replace window.Horizon.basePath="/"+window.Horizon.path; with window.Horizon.basePath="/[you sub-directoy]/"+window.Horizon.path;
It should work...until you run update one day and it mysteriously stop working.

How can I create routes similar to Auth::routes()

I created a laravel package with routes. I want to do something similar to Laravel's authentication scaffolding Auth::routes(), where they are injected into whatever file you want to include them (i.e. api.php or web.php).
I am currently using
public function boot()
{
...
$this->loadRoutesFrom(__DIR__.'/routes/api.php');
...
}
But this makes the routes available from anywhere, which I do not want.
I understand I should use a Service Container, but this is my first package and first time creating my own Service Provider, so I am not too sure how to do so, and I couldn't find documentation on how to use these for routes.
Instead, I want to be able to do something along the lines of:
//routes/api.php
Route::group(['prefix'=>'v1', 'middleware:auth-api'], function(){
Logging::routes(); //<-----
...
});
Simple solution, make a class that has a static method that declares your routes.
// your/package/Logging.php
class Logging
{
public static method routes()
{
...your routes...
}
}
// routes/web.php
use Your/Package/Logging;
Logging::routes();

Laravel - Share data with all views when the data is available

I'm writing a web app using Laravel 5.6. I need a list of all the connections the current session user have, in all the views.
I tried something like this
View::share('connections', Connection::getList(Auth::id()))
I put this code inside the boot function of AppServiceProvider. But the problem arises when the user isn't already logged in, because at that time Auth::id() is set to null.
The connection list is not generated when the user logs in. This throws the following error:
connections variable is not defined.
This target can achieve through different method,
1. Using BaseController
The way I like to set things up, I make a BaseController class that extends Laravel’s own Controller, and set up various global things there. All other controllers then extend from BaseController rather than Laravel’s Controller.
class BaseController extends Controller
{
public function __construct()
{
//its just a dummy data object.
$user = User::all();
// Sharing is caring
View::share('user', $user);
}
}
2. Using Filter
If you know for a fact that you want something set up for views on every request throughout the entire application, you can also do it via a filter that runs before the request — this is how I deal with the User object in Laravel.
App::before(function($request)
{
// Set up global user object for views
View::share('user', User::all());
});
OR
You can define your own filter
Route::filter('user-filter', function() {
View::share('user', User::all());
});
and call it through simple filter calling.
Update According to Version 5.*
3. Using View Composer
View Composer also help to bind specific data to view in different ways. You can directly bind variable to specific view or to all views. For Example you can create your own directory to store your view composer file according to requirement. and these view composer file through Service provide interact with view.
View composer method can use different way, First example can look alike:
You could create an App\Http\ViewComposers directory.
Service Provider
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class ViewComposerServiceProvider extends ServiceProvider {
public function boot() {
view()->composer("ViewName","App\Http\ViewComposers\TestViewComposer");
}
}
After that, add this provider to config/app.php under "providers" section.
TestViewComposer
namespace App\Http\ViewComposers;
use Illuminate\Contracts\View\View;
class TestViewComposer {
public function compose(View $view) {
$view->with('ViewComposerTestVariable', "Calling with View Composer Provider");
}
}
ViewName.blade.php
Here you are... {{$ViewComposerTestVariable}}
This method could help for only specific View. But if you want trigger ViewComposer to all views, we have to apply this single change to ServiceProvider.
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class ViewComposerServiceProvider extends ServiceProvider {
public function boot() {
view()->composer('*',"App\Http\ViewComposers\TestViewComposer");
}
}
Reference

Laravel get Auth variable for both - api token and session based authentication

I am using laravel 5.2.
Recently, I've updated Auth module to have session based authentication for web and api_token based authentication for external api calls.
Now, I am finding error in using Auth::id() and Auth::user() where I've used api_token based authentication. So that I am forced to use Auth::guard('api')->id() and Auth::guard('api')->user() methods instead.
Now, my question is, is there any common method that I can use for both irrespective of api_token based authentication or session based? What about auth()->user() and auth()->id()?
What if am I using the any method for both of the authentication? For example, methodA() is used within api_token based authentication as well as in session based too, how can I handle that case if I required to use Auth variable?
I think that controllers, that handle regular requests (through session-based authentication), should be separate from api controllers (token-based authentication). So, each controller would have responsibility over a single part of the functionality. Also, changes in api controller will not have side effect in session controller. Therefore, you can specify auth guard explicitly in each controller. Laravel requires specifying guard explicitly, otherwise default guard will be used. There is no way to make intelligent guess about what guard to use natively. Of course, you can make something like this:
public function action(Request $request)
{
$guard = $request->has('api_token') ? 'api' : 'session';
$authUser = Auth::guard($guard)->user();
//your code next
}
If you will go with separate controllers you can generalize common functionality into parent abstract controller. Note, in example below ChildControllers differs only by namespace.
Parent:
<?php
namespace App\Http\Controllers\Api
use App\Http\Controllers\Controller;
abstract class ParentController extends Controller
{
public function action(Request $request)
{
$authUser = Auth::guard($this->guard)->user();
//your code...
}
}
API controller:
<?php
namespace App\Http\Controllers\Api
use App\Http\Controllers\ParentController
class ChildController extends ParentController
{
protected $guard = 'api';
//your code...
}
Session Controller:
<?php
namespace App\Http\Controllers\Session
use App\Http\Controllers\ParentController
class ChildController extends ParentController
{
protected $guard = 'session';
//your code...
}

Resources