I made a package which is counting the visitors on a webpage. Currently I have a single route, controller and view which don't do very much other than display a simple string. I have a separate Laravel application where this package is specifically build for. In this separate application I have a layout file called backend.
layouts/layouts/backend.blade.php.
My package view is extending this template like so: (backend.blade.php does not exists in the package but in the separate laravel application of-course)
#extends('layouts.layouts.backend')
#section('content')
<div class="container-fluid pt-5 ">
<div class="row">
<div class="col-md-6">
<h3>{{ __('Visitors') }}</h3>
</div>
</div>
</div>
#endsection
The package successfully extends this layout but it can't find functions such as Auth::user()->token and it will say
Trying to get property 'token' of non-object (View: /Users/rainierlaan/Sites/rainierlaan/resources/views/layouts/layouts/backend.blade.php)
Why does this happen?
This is my packages service provider
public function register()
{
// Controllers
$this->app->make('Rainieren\Visitors\Http\Controllers\VisitorController');
// Views
$this->loadViewsFrom(__DIR__.'/resources/views', 'visitors');
$this->publishes([
__DIR__.'/resources/views' => resource_path('views/visitors'),
]);
// Migrations
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
include __DIR__ . '/routes/routes.php';
}
When I do vendor:publish the view successfully publishes to the right folder but somehow can't recognise functions such as Auth::user()->token or Auth::user()->unreadNotifications->count())
This is my package route:
<?php
Route::get('dashboard/visitors', '\Rainieren\Visitors\Http\Controllers\VisitorController#index')->name('visitors');
And this is the controller
public function index()
{
return view('visitors::index');
}
I would need a little bit more debugging information but my first assumption is the missing AuthenticateSession or Authenticate middleware.
Laravel defines a default middleware group web for your routes inside routes/web.php and this group uses the AuthenticateSession middleware. This is how a new installation looks like:
Route::group([
'middleware' => 'web', <<< this is the magic part
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
Here we see that the middleware group web is used.
In your custom module / service provider this is not the case. Your Route::get() definition is added to the Router but not inside this group. Therefore all necessary internals to authenticate the user are not performed.
In this scenario I would try to use ->middleware('auth') or ->middleware('web') which will use the main-projects group middleware.
Route::get('dashboard/visitors', '\Rainieren\Visitors\Http\Controllers\VisitorController#index')
->name('visitors')
->middleware('web');
Here is a different idea:
If you say you're always authenticated. Then you could try to move all web middlewares into the global middlewares within your Kernel (protected $middleware = []).
I did not tested this but I can imagine that this could work as well.
I have seen in all of my active packages that they don't use Auth class at all in the views.
Their workaround is using this in the controller and pass it to the view:
$user = auth()->user();
return view('folder.view' ,['user'=>$user]);
And then in the view:
{{$user->token}} <!-- OR --!> {{$user->unreadNotifications->count()}}
Obviously as #N69S has stated only if a user is authenticated this is going to work.
Hope this works!!
EDIT:(Sorry didn't understand well the first time)
The error is in your service provider class yo have to do all the logic on the boot function, not in the register, because if you do in the register function, you load all before the laravel framework (To say something)
https://laravel.com/docs/5.7/providers#the-register-method
Your serviceprovider should end like this:
public function register()
{
//Nothing
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
// Controllers
$this->app->make('Rainieren\Visitors\Http\Controllers\VisitorController');
// Views
$this->loadViewsFrom(__DIR__.'/resources/views', 'visitors');
$this->publishes([
__DIR__.'/resources/views' => resource_path('views/visitors'),
]);
// Migrations
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
include __DIR__ . '/routes/routes.php';
}
Hope this finally works!
Related
I really can't think of an elegant solution to fix this problem so here it is:
I followed this tutorial https://www.youtube.com/watch?v=KqzGKg8IxE4 to create a localization option for my website. However, integrating the auth was cancer and now I'm faced with an even greater issue regarding the routing for pages that require a get parameter.
Here is my blade code inside the app.blade.php for the language switcher:
<language-switcher
locale="{{ app()->getLocale() }}"
link-en="{{ route(Route::currentRouteName(), 'en') }}"
link-bg="{{ route(Route::currentRouteName(), 'bg') }}"
></language-switcher>
And here are some of my routes
Route::redirect('/', '/en');
Route::get('email/verify', 'Auth\VerificationController#show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController#verify')->name('verification.verify');
Route::get('email/resend', 'Auth\VerificationController#resend')->name('verification.resend');
Route::group([
"prefix" => '{language}',
'where' => ['language' => '(en||bg)'],
], function () {
//Auth routes
Auth::routes();
//Returns to home
Route::get('/', 'HomeController#index')->name('home');
//Handles the faq routes
Route::resource('faq', 'FaqController');
});
Route::get('password/reset/{token}', 'Auth\ResetPasswordController#showResetForm')->name('password.reset');
Route::post('password/reset', 'Auth\ResetPasswordController#reset')->name('password.update');
});
It works well for pages that don't require any get parameters to work with ex: Login / Register / Home page, however if i use something that requires like, faq.edit, which takes an {id} parameters - the language switcher will throw an error because I haven't passed an {id} parameter to it.
The only solution I can think of is adding the language-switcher inside the child blade view and from there I pass the required parameters, however that implies that I have to add the language-switcher to every child view instead of only once at the parent.
You can achieve it with the following steps:
Implement a URL generator macro which will make much easier to generate your URLs which must be identical except language
Get the current route's parameters
Merge the chosen language into them
<?php
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
UrlGenerator::macro('toLanguage', function (string $language) {
$currentRoute = app('router')->current();
$newRouteParameters = array_merge(
$currentRoute->parameters(), compact('language')
);
return $this->route($currentRoute->getName(), $newRouteParameters);
});
}
}
And in your Blade file
#inject('app', 'Illuminate\Contracts\Foundation\Application')
#inject('urlGenerator', 'Illuminate\Routing\UrlGenerator')
<language-switcher
locale="{{ $app->getLocale() }}"
link-en="{{ $urlGenerator->toLanguage('en') }}"
link-bg="{{ $urlGenerator->toLanguage('bg') }}"
></language-switcher>
You'll notice I used contracts injection instead of using facades. More informations here https://laravel.com/docs/7.x/contracts#contracts-vs-facades
And if you don't known Laravel macros, more informations here https://tighten.co/blog/the-magic-of-laravel-macros
Using Laravel 7.
In the controller constructor, I then hoped to get access to the current user details so I could load main site widgets (buttons links etc) and custom user widgets in to one to be displayed in the view
use Illuminate\Support\Facades\Auth;
...
$widgets = Cache::get("widgets");
$usersdata = Cache::get("userdata");
$this->middleware('auth');
$widgets = array_merge($widgets, $usersdata[Auth::user()->id]["widgets"]);
View::share([
"widgets" => json_encode($widgets)
]);
however at this stage from research the user data is not available (even after authentication ?).
Not sure of best way to access this, or better practice might be to override the middleware auth (where?) so that it could return user id or something eg:
$userid=$this->middleware('auth');
I would like this in the constructor so the same method is in place for all controllers which extend this main controller.
This is intended behavior from laravel, you can read more about it here.
Laravel collects all route specific middlewares first before running
the request through the pipeline, and while collecting the controller
middleware an instance of the controller is created, thus the
constructor is called, however at this point the request isn’t ready
yet.
You can find Taylor's reasoning behind it here:
It’s very bad to use session or auth in your constructor as no request
has happened yet and session and auth are INHERENTLY tied to an HTTP
request. You should receive this request in an actual controller
method which you can call multiple times with multiple different
requests. By forcing your controller to resolve session or auth
information in the constructor you are now forcing your entire
controller to ignore the actual incoming request which can cause
significant problems when testing, etc.
So one solution would be to create a new middleware and then apply it to all routes, something like this, where widgets is your new middleware:
Route::group(['middleware' => ['auth', 'widgets']], function () {
// your routes
});
But if you really want to keep it in the constructor you could implement the following workaround:
class YourController extends Controller
{
public function __construct(Request $request)
{
$this->middleware('auth');
$this->middleware(function ($request, $next) {
$widgets = Cache::get("widgets");
$usersdata = Cache::get("userdata");
$widgets = array_merge($widgets, $usersdata[$request->user()->id]["widgets"]);
View::share([
"widgets" => json_encode($widgets)
]);
return $next($request);
});
}
}
I'm displaying a front-end countdown of when the user's session will expire, I want to request the time left without updating it.
Here's what I have so far:
$ttl = Redis::ttl(config('cache.prefix') . ':' . Session::getId());
return response()->json($ttl);
Each time this is requested the ttl is reset back to the session.lifetime value.
I solved this by extending the StartSession middleware:
class StartSession extends \Illuminate\Session\Middleware\StartSession
{
public function terminate($request, $response)
{
if (!$request->is('auth/ping')) {
parent::terminate($request, $response);
}
}
}
Where auth/ping is the route I don't want the session to save on.
I then registered this in the app container as a singleton, so the terminate method resolves to the same instance:
In AppServiceProvider->register:
$this->app->singleton('App\Http\Middleware\StartSession');
The existing mapWebRoutes() method in app/Providers/RouteServiceProvider.php looks like this.
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* #param \Illuminate\Routing\Router $router
* #return void
*/
protected function mapWebRoutes(Router $router)
{
$router->group([
'namespace' => $this->namespace, 'middleware' => 'web',
], function ($router) {
require app_path('Http/routes.php');
});
}
You can simply add something like the following to that method, or you could duplicate the code above which loads routes.php and remove the web middleware.
$router->get('session-ttl', function () {
return response()->json(
\Redis::ttl(config('cache.prefix') . ':' . cookie(config('session.cookie')));
);
});
Or
$router->group([
'namespace' => $this->namespace
], function ($router) {
require app_path('Http/routes_wo_web.php');
});
The session stuff is handled by the Illuminate\Session\Middleware\StartSession middleware, which is in the web middleware group.
The simplest solution would be to place your route outside the standard web middleware group.
If you need any of the other web middlewares, you can add them back to the route.
To actually use the session in your route, you have a couple of options:
you can make a Session middleware that does not terminate the session (extend the existing one, and override the handle method to just return $next($request))
someone here's suggested that as it's a one off route, you could just start the session manually wherever you're handling your route (nick the code from the existing middleware - it's only a few lines).
I think the recommended solution would be to do it in a middleware, though.
I have a partial view in master layout which is the navigation bar. I have a variable $userApps. This variable checks if the user has enabled apps (true), if enabled then I would like to display the link to the app in the navigation bar.
homepage extends master.layout which includes partials.navbar
My code in the navbar.blade.php is this:
#if ($userApps)
// display link
#endif
However I get an undefined variable error. If I use this in a normal view with a controller it works fine after I declare the variable and route the controller to the view. I dont think I can put a controller to a layout since I cant route a controller to a partial view, so how do I elegantly do this?
What version of Laravel you use? Should be something like this for your case:
#include('partials.navbar', ['userApps' => $userApps])
Just for a test purpose, I did it locally, and it works:
routes.php
Route::get('/', function () {
// passing variable to view
return view('welcome')->with(
['fooVar' => 'bar']
);
});
resources/views/welcome.blade.php
// extanding layout
#extends('layouts.default')
resources/views/layouts/default.blade.php
// including partial and passing variable
#include('partials.navbar', ['fooVar' => $fooVar])
resources/views/partials/navbar.blade.php
// working with variable
#if ($fooVar == 'bar')
<h1>Navbar</h1>
#endif
So the problem must be in something else. Check your paths and variable names.
The other answers did not work for me, or seem to only work for older versions. For newer versions such as Laravel 7.x, the syntax is as follows.
In the parent view:
#include('partial.sub_view', ['var1' => 'this is the value'])
In the sub view:
{{ $var1 }}
I have gone through all the answers but below is the best way to do because you can also run queries in serviceProvider.
You need to create a separate service or you can use AppServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
View::composer('layouts.admin-layout', function ($view) {
$view->with('name', 'John Doe');
});
}
}
In your layout
{{$name}}
This approach is very simple:
In parent view :
#include('partial.sub_view1', ['This is value1' => $var1])
In sub view :
{{ $var1 }}
You can use view composer to send your variable to partial view.
Check the laravel documentation on laravel.com about view composer.
Also you can check the following link that will help you resolve this problem.
https://scotch.io/tutorials/sharing-data-between-views-using-laravel-view-composers
I am building APIs for my Android app using laravel and default session driver set to REDIS.
I found a good article here http://dor.ky/laravel-prevent-sessions-for-routes-via-a-filter/ which sort of serves the purpose.
However when ever I hit the url it also hits the redis and generates the key which is empty. Now I want avoid creating empty session keys in redis. Ideally it should not hit the redis How can I do that?
Can we customise sessios in a way so that sessions are generated only for specific routes (or disable for specific routes)?
I can explain more with specific use case, please let me know.
Its really easy using the middleware in Laravel 5, I needed any request with an API key not to have a session and I simply did :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\Middleware\StartSession as BaseStartSession;
class StartSession extends BaseStartSession
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(\Request::has('api_key'))
{
\Config::set('session.driver', 'array');
}
return parent::handle($request, $next);
}
}
Also you will need to extend the SessionServiceProvider as follows:
<?php namespace App\Providers;
use Illuminate\Session\SessionServiceProvider as BaseSessionServiceProvider;
class SessionServiceProvider extends BaseSessionServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton('App\Http\Middleware\StartSession');
}
}
and place in your config/app.php under providers:
'App\Providers\SessionServiceProvider',
Also you must change it in your kernel file: App/Http/Kernel.php, in the $middlewareGroups section change the default entry, \Illuminate\Session\Middleware\StartSession::class, to your new class \App\Http\Middleware\StartSession::class,.
In Laravel 5, just don't use the StartSession, ShareErrorsFromSession, and VerifyCsrfToken middlewares.
In my application I've moved these three middlewares from the web group to a new stateful group, and then I have included this stateful group on routes which need to know about the session (in addition to web in all cases, in my app at least). The other routes belong to either the web or api groups.
Now when making requests to the routes which are not using the stateful middleware group session cookies are not sent back.
The simplest way to achieve this is to Make your own AppStartSession middleware that subclasses Illuminate\Session\Middleware\StartSession and the replace the class being used in kernel.php. The only method you need to override in your subclass is sessionConfigured() for which you can return false to disable the session or parent::sessionConfigured() to allow it.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\Middleware\StartSession;
class AppStartSession extends StartSession
{
protected function sessionConfigured(){
if(!\Request::has('api_key')){
return false;
}else{
return parent::sessionConfigured();
}
}
}
kernel.php (see *** comment for where the change is done)
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* #var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// *** Replace start session class
// \Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\AppStartSession::class,
// *** Also comment these ones that depend on there always being a session.
//\Illuminate\View\Middleware\ShareErrorsFromSession::class,
//\App\Http\Middleware\VerifyCsrfToken::class,
];
/**
* The application's route middleware.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];
}
Don't fight the framework, embrace it!
Since Laravel 5.2, when middleware groups were introduced, you may disable session for certain routes by defining them outside of the "web" middleware group (which includes the StartSession middleware responsible for session handling). As on latest 5.2.x versions the whole default routes.php file is wrapped with "web" middleware group, you need to make some modification in app/Providers/RouteServiceProvider.php file, as described here.
There appears to be a way to accomplish this using a session reject callback.
Relevant sources...
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Foundation/Application.php#L655
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Foundation/Application.php#L660
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Session/Middleware.php#L60
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Session/Middleware.php#L97
I can't find many references to this around the web, but reading more through the source it appears that if the session reject callback returns a truthy value, the session will be forced to use an array driver for the request rather than whatever is configured. Your callback also gets the current request injected so you can do some logic based on the request parameters.
I've only tested this on a local Laravel 4.2 install but it seems to work. You just need to bind a function to session.reject.
First, create a SessionRejectServiceProvider (or something like that)
<?php
use \Illuminate\Support\ServiceProvider;
class SessionRejectServiceProvider extends ServiceProvider {
public function register()
{
$me = $this;
$this->app->bind('session.reject', function($app)use($me){
return function($request)use($me){
return call_user_func_array(array($me, 'reject'), array($request));
};
});
}
// Put the guts of whatever you want to do in here, in this case I've
// disabled sessions for every request that is an Ajax request, you
// could do something else like check the path against a list and
// selectively return true if there's a match.
protected function reject($request)
{
return $request->ajax();
}
}
Then add it to your providers in your app/config/app.php
<?php
return array(
// ... other stuff
'providers' => array(
// ... existing stuff...
'SessionRejectServiceProvider',
),
);
Edit / More Info
The net result is that the reject() method is called on every request to your application, before the session is started. If your reject() method returns true, sessions will be set to the array driver and basically do nothing. You can find a lot of useful info the $request parameter to determine this, here's the API reference for the request object in 4.2.
http://laravel.com/api/4.2/Illuminate/Http/Request.html
I've been trying to accomplish a similar feature.
Our API is stateless except for 1 route - the version 1 cart.
I ended up with setting 'driver' in the app/config/session.php like this ...
'driver' => 'v1/cart' === Request::getDecodedPath() ? 'native' : 'array',
Nothing magic. Initially we though of using a before filter, but that wasn't happening early enough.
It seems a simple way to do things, but I may be missing something.
Putting the switch in the config seems an easy place for other developers to see what the driver is whereas putting it in a service provider is so tucked out of the way, without knowing what service providers are installed and what they interact with, it would be far harder to debug.
Anyway. Hope this is of some use.
As pointed out below ... DO NOT CACHE YOUR CONFIG IF IT IS DYNAMIC.
Which does lead to it being of limited use. As soon as we no longer need to support v1/cart, we will be dropping this route and then be back on a static config.
Laravel default have two routes group called web and api, the api routes group default without session.
So, we can write any route role to routes/api.php, will not use session default.
If not want to use the api prefix url, we can modify app\Providers\RouteServiceProvider add a new group like this:
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/static.php'));
Now you can place any routes into routes/static.php file will not to use session.
Hope helpful.
Laravel 5x
In the App\Providers\RouteServiceProvider file, just copy the mapApiRoutes() method to a new method called mapStaticRoutes(), remove the prefix('api') call, and add "routes/static.php" (you will need to create this file). This will use the same stateless "api" middleware and not have an /api prefix assigned to the routes.
protected function mapStaticRoutes()
{
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/static.php'));
}
Just update the "map()" method to call "$this->mapStaticRoutes();" so that it knows of your new file. And any route added there should now be stateless and it wasn't much work.....
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
// Static Routes (stateless, no /api prefix)
$this->mapStaticRoutes();
}
static.php
// Health Check / Status Route (No Auth)
Route::get('/status', function() {
return response()->json([
'app' => 'My Awesome App',
'status' => 'OK'
]);
});