While building multi-tenancy packages for Laravel 5 I had to find out how to add middleware dynamically from code. In comparison to this question on SO I do not want to touch the Http/Kernel definitions.
During application initialization I check whether the requested hostname is known in the database and whether this hostname requires a redirect to a primary hostname or ssl.
Because you don't want to touch the Http/Kernel as a package, we need to use the service provider.
Requirements:
dynamically add middleware without touching Http/Kernel
use service provider and response object instead of "hacks"
The solution is to dynamically register the middleware in the kernel. First write your middleware, for instance:
<?php namespace HynMe\MultiTenant\Middleware;
use App;
use Closure;
use Illuminate\Contracts\Routing\Middleware;
class HostnameMiddleware implements Middleware
{
public function handle($request, Closure $next)
{
/* #var \HynMe\MultiTenant\Models\Hostname */
$hostname = App::make('HynMe\Tenant\Hostname');
if(!is_null($redirect = $hostname->redirectActionRequired()))
return $redirect;
return $next($request);
}
}
Now in your service provider use the following code in the boot() method to add this middleware to the Kernel:
$this->app->make('Illuminate\Contracts\Http\Kernel')->prependMiddleware('HynMe\MultiTenant\Middleware\HostnameMiddleware');
To answer what redirectActionRequired() method does in the hostname object:
/**
* Identifies whether a redirect is required for this hostname
* #return \Illuminate\Http\RedirectResponse|null
*/
public function redirectActionRequired()
{
// force to new hostname
if($this->redirect_to)
return $this->redirectToHostname->redirectActionRequired();
// #todo also add ssl check once ssl certificates are support
if($this->prefer_https && !Request::secure())
return redirect()->secure(Request::path());
// if default hostname is loaded and this is not the default hostname
if(Request::getHttpHost() != $this->hostname)
return redirect()->away("http://{$this->hostname}/" . (Request::path() == '/' ? null : Request::path()));
return null;
}
If you need to dynamically register routeMiddleware use the following in your service provider;
$this->app['router']->middleware('shortname', Vendor\Some\Class::class);
Please add comments if you have questions about this implementation.
Related
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.
I'm building a Laravel app, which has a number of various features. I want to be able to enable or disable them depending on a particular domain's requirement. Currently, I have in my config a series of flags such as:
'is_feature_1_enabled' => true,
'is_feature_2_enabled' => false,
... and so on.
Then in my controllers and views, I check those config values to see whether or not I should be displaying something, allowing certain actions, etc. My app is starting to get polluted with these kinds of checks everywhere.
Is there a best practice method of managing features in a Laravel app?
This is technically called feature flags - https://martinfowler.com/articles/feature-toggles.html
depends on your requirements, flags in config/database, rollout, etc...
But it's basically if's in code and cannot be clean.
Laravel packages:
https://github.com/alfred-nutile-inc/laravel-feature-flag
https://github.com/francescomalatesta/laravel-feature
Some services:
https://launchdarkly.com/
https://bullet-train.io/
https://configcat.com/
Also look at https://marketingplatform.google.com/about/optimize/ for frontend.
I've encountered the same problem when I tried to implement multiple hotel providers.
What I did was using service container.
first you will create class for each domain With his features:
like Doman1.php ,Domain2.php
then inside each one of those you will add your logic.
then you gonna use binding in your app service provider to bind the domain with class to use.
$this->app->bind('Domain1',function (){
return new Domain1();
});
$this->app->bind('Domain2',function (){
return new Domain2();
});
Note you can use general class that holds the features goes with all domains then use that general class in your classes
Finally in your controller you can check your domain then to use the class you gonna use
app(url('/'))->methodName();
Look like you are hard coding things based on config values to enable or disable certain features. I recommend you to control things based on named routes rather than config value.
Group all the route as a whole or by feature wise.
Define name for all routes
Control the enable/disable activity by route name and record in database
Use Laravel middleware to check whether a particular feature is enabled or disabled by getting the current route name from request object and matching it with the database..
so you will not have the same conditions repeating every where and bloat your code..
here is a sample code show you how to retrieve all routes, and you can match the route group name to further process to match your situation.
Route::get('routes', function() {
$routeCollection = Route::getRoutes();
echo "<table >";
echo "<tr>";
echo "<td width='10%'><h4>HTTP Method</h4></td>";
echo "<td width='10%'><h4>Route</h4></td>";
echo "<td width='80%'><h4>Corresponding Action</h4></td>";
echo "</tr>";
foreach ($routeCollection as $value) {
echo "<tr>";
echo "<td>" . $value->getMethods()[0] . "</td>";
echo "<td>" . $value->getPath() . "</td>";
echo "<td>" . $value->getName() . "</td>";
echo "</tr>";
}
echo "</table>";
});
and here is a sample middleware handler where you can check whether a particular feature is active by matching with what you have already stored in your database..
public function handle($request, Closure $next)
{
if(Helper::isDisabled($request->route()->getName())){
abort(403,'This feature is disabled.');
}
return $next($request);
}
Assuming that those features are only needed for HTTP requests.
I would create a default Features base class with all the default flags:
Class Features {
// Defaults
protected $feature1_enabled = true;
protected $feature2_enabled = true;
public function isFeature1Enabled(): bool
{
return $this->feature1_enabled;
}
public function isFeature2Enabled(): bool
{
return $this->feature2_enabled;
}
}
Then I would extend that class for each Domain and set the overrides that are needed for that domain:
Class Domain1 extends Features {
// override
protected $feature1_enabled = false;
}
Then create a Middleware to bind the Features Class to the container:
class AssignFeatureByDomain
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
switch ($_SERVER['HTTP_HOST']) {
case 'domain1':
app()->bind(Features::class, Domain1::class);
break;
default:
abort(401, 'Domain rejected');
}
return $next($request);
}
}
Don't forget to attach this middleware to your routes: to a group or for each route.
After this you can TypeHint your Features class in your controllers:
public function index(Request $request, Features $features)
{
if ($features->isFeature1Enabled()) {
//
}
}
Laravel is great with this, you can even store your features in db, and create a relation between the domain.
I would recommend to use Gates and Policies, which will give you better control in your controllers and blade templates. This means you register the gates from your db or hard code them.
For example if you have export products feature with a button in your system and you want to make that feature available to some users you can register gates with business logic.
//Only admins can export products
Gate::define('export-products', function ($user) {
return $user->isAdmin;
});
Then you can do the following in the controllers
<?php
namespace App\Http\Controllers;
use App\Product;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ProductsController extends Controller
{
/**
* Export products
*
* #param Request $request
* #param Post $post
* #return Response
* #throws \Illuminate\Auth\Access\AuthorizationException
*/
public function export(Request $request)
{
$this->authorize('export-products');
// The current user can export products
}
}
Here is an example for your blade templates:
#can('export-products', $post)
<!-- The Current User Can export products -->
#endcan
#cannot('export-products')
<!-- The Current User Can't export products -->
#endcannot
more information available at https://laravel.com/docs/5.8/authorization
Interesting case you have here. It might be interesting to look into a Feature interface or abstract class that contains a few methods you generally need.
interface Feature
{
public function isEnabled(): bool;
public function render(): string;
// Not entirely sure if this would be a best practice but the idea is to be
// able to call $feature->execute(...) on any feature.
public function execute(...);
...
}
You could even devide these into ExecutableFeature and RenderableFeature.
Further on some kind of factory class could be made to make life easier.
// Call class factory.
Feature::make('some_feature')->render();
...->isEnabled();
// Make helper method.
feature('some_feature')->render();
// Make a blade directives.
#feature('some_feature')
#featureEnabled('some_feature')
What I did in my case was creating a new table on the database, you could call it Domains for instance.
Add all the specific features, those which could be shown on some domains but not in the rest, as columns for that table as bit for boolean values. Like, in my case, allow_multiple_bookings, use_company_card... whatever.
Then, consider creating a class Domain and its respective repository, and just ask these values on your code, trying to push as much as possible that logic into your domain (your model, application services, etc).
For instance, I would not do the check on the controller method for RequestBooking if the domain which is requesting a booking can request only one or more.
Instead I do it on a RequestBookingValidatorService which can check if the booking datetime has passed, the user has an enabled credit card, ... or the Domain which this action comes from is allowed to request more than one booking (and then if it already has any).
This adds the convenience of readability, as you have pushed this decision to your application services. Also, I find that whenever I need a new feature I can use Laravel (or Symfony) migrations to add that feature on the table and I could even update its rows (your domains) with the values I want on the same commit I coded.
I'm using Laravel 5.5. The nature of the website is a 'multisite' architecture where multiple websites/domains are run from the same codebase.
I've come across an issue when sending email. I need to change the from name and address as well as the transport (SMTP, etc) options depending on which website is being viewed. I have these details stored in a config file.
The easiest way is to just pull those details in the Controller before I call Mail::send/Mail::queue and to update them. However, this brings back 2 issues:
There is a heavy reliance on remembering to actually do that every time I send any email in the code. In short, it's not abiding by DRY.
I'd be forced to use Mail::send instead of Mail::queue, because the queue wouldn't have any idea of the config update from the time it was queued only from when it is processed .
How can I achieve what I am looking to do here in a clean way?
I thought about extending all of my 'Mailable' classes with a custom class that updates the SMTP details, but it doesn't look like you can update the SMTP/Transport information after the class is initiated; you can only update the from name and address.
I managed to find a way to do this.
I had my mailable class (ContactFormMailable) extend a custom class, as follows:
<?php
namespace CustomGlobal\Mail;
use CustomGlobal\Mail\CustomMailable;
use CustomGlobal\ContactForm;
class ContactFormMailable extends CustomMailable
{
public $contact_form;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(ContactForm $contact_form)
{
$this->contact_form = $contact_form;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$view = $this->get_custom_mail_view('contact_form', $this->contact_form);
return $this->subject('Contact Form Enquiry')
->view($view);
}
}
You'll notice I'm calling get_custom_mail_view. This is in my extended class and used to calculate the view and template I need to use for my mail, depending on the website being viewed. In here I also set the location of my config folder.
<?php
namespace CustomGlobal\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use Swift_Mailer;
use Swift_SmtpTransport;
use CustomGlobal\Website;
use CustomGlobal\Territory;
class CustomMailable extends Mailable
{
use Queueable, SerializesModels;
public $layout_view_to_serve;
public $host_folder;
/**
* Override Mailable functionality to support per-user mail settings
*
* #param \Illuminate\Contracts\Mail\Mailer $mailer
* #return void
*/
public function send(Mailer $mailer)
{
app()->call([$this, 'build']);
$config = config($this->host_folder .'.mail');
// Set SMTP details for this host
$host = $config['host'];
$port = $config['port'];
$encryption = $config['encryption'];
$transport = new Swift_SmtpTransport( $host, $port, $encryption );
$transport->setUsername($config['username']);
$transport->setPassword($config['password']);
$mailer->setSwiftMailer(new Swift_Mailer($transport));
$mailer->send($this->buildView(), $this->buildViewData(), function ($message) use($config) {
$message->from([$config['from']['address'] => $config['from']['name']]);
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->buildAttachments($message)
->runCallbacks($message);
});
}
/**
* Calculate the template we need to serve.
* $entity can be any object but it must contain a
* $website_id and $territory_id, as that is used
* to calculate the path.
*/
public function get_custom_mail_view($view_filename, $entity)
{
if(empty($view_filename)) {
throw new Exception('The get_custom_mail_view method requires a view to be passed as parameter 1.');
}
if(empty($entity->website_id) || empty($entity->territory_id)) {
throw new Exception('The get_custom_mail_view method must be passed an object containing a website_id and territory_id value.');
}
// Get the website and territory
$website = Website::findOrFail($entity->website_id);
$territory = Territory::findOrFail($entity->territory_id);
$view_to_serve = false;
$layout_view_to_serve = false;
// Be sure to replace . with _, as Laravel doesn't play nice with dots in folder names
$host_folder = str_replace('.', '_', $website->website_domain);
$this->host_folder = $host_folder; // Used for mail config later
/***
Truncated for readability. What's in this area isn't really important to this answer.
***/
$this->layout_view_to_serve = $layout_view_to_serve;
return $view_to_serve;
}
}
It's important to remember that mail can be queued. If you do this is another way, such as setting a config at runtime, then you'll find that the process that runs the queue has no visibility/scope of your runtime config changes, and you'll end up firing out email from your default values.
I found a few answers similar to this one, which helped me out, but none of them worked completely, and some are out-dated (Swift_SmtpTransport is changed considerably since those answers).
Hopefully this helps someone else out.
I want to implement some extra features to Illuminate\Translate\Translator.
So, i create my folder in ~/vendor directory, place there My/Traslator class, that will implement Symfony\Component\Translation\TranslatorInterface. Right?
Is it OK to extend laravel translator class (a lot of functionality will be duplicated otherwise) in my package?
If it is ok - it will be necessary to tie to current laravel version to keep code stable. But what will happen in case enduser laravel version will differ from one required in my package?
What should i do then to make laravel use my translator class in application (facades,etc)?
Make a Translator class and make it extend Illuminate\Translation\Translator
<?php
namespace App\Helpers;
use Illuminate\Translation\Translator as LaravelTranslator;
class Translator extends LaravelTranslator
{
// here you can overwrite any functions you want/need
}
Create your own TranslationServiceProvider inside app/providers (just copy the laravel translation service provider and change the line where it uses Translator with your own Translator class where you have overwritten what you needed)
<?php
namespace App\Providers;
use App\Helpers\Translator; // <= Your own class
use Illuminate\Translation\FileLoader;
use Illuminate\Support\ServiceProvider;
class TranslationServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
/**
* Register the translation line loader.
*
* #return void
*/
protected function registerLoader()
{
$this->app->singleton('translation.loader', function ($app) {
return new FileLoader($app['files'], $app['path.lang']);
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return ['translator', 'translation.loader'];
}
}
Comment out or delete the Laravels translator service line inside config/app.php:
//Illuminate\Translation\TranslationServiceProvider::class,
Add your own Provider in that same array
App\Providers\TranslationServiceProvider::class,
This page has more information: http://laravel.com/docs/5.0/extending#container-based-extension
So what you need to do is:
Extend the built-in class from the vendor directory
Create a new service provider that add your translation class to the service container
Replace Laravel’s translation service provider in your config/app.php file with the namespace of your translation service provider
Now when you ask for the translation service provider out of the service container—either directly (app('translator')) or with the Lang façade, it will return your translation class rather than Laravel’s.
My basic aim is to extend the package class and override a method in it.
I have used Fnatte's ans as a reference : How to extend laravel 4 core?
Sentry2 is a package that i am using for authentication in larval 4.
A user can be logged in using Sentry::login($credentials)
I want to override the login method of the Sentry package and remove the check for activating the user(i have commented it in the code below)
public function login(UserInterface $user, $remember = false)
{
#prevent throwing error if not activated !
// if ( ! $user->isActivated())
// {
// $login = $user->getLogin();
// throw new UserNotActivatedException("Cannot login user [$login] as they are not activated.");
// }
$this->user = $user;
// Create an array of data to persist to the session and / or cookie
$toPersist = array($user->getId(), $user->getPersistCode());
// Set sessions
$this->session->put($toPersist);
if ($remember)
{
$this->cookie->forever($toPersist);
}
// The user model can attach any handlers
// to the "recordLogin" event.
$user->recordLogin();
}
Steps i have done till now :
1. Created a app/lib folder and added my extension class CustomSentry in it.
2. Added the app/lib folder to composer.json class map
app/lib/CustomSentry.php :
use Cartalyst\Sentry\Sentry;
use Cartalyst\Sentry\Users\UserInterface;
class CustomSentry extends Sentry{
public function login(UserInterface $user, $remember = false){
$this->user = $user;
// Create an array of data to persist to the session and / or cookie
$toPersist = array($user->getId(), $user->getPersistCode());
// Set sessions
$this->session->put($toPersist);
if ($remember)
{
$this->cookie->forever($toPersist);
}
// The user model can attach any handlers
// to the "recordLogin" event.
$user->recordLogin();
}
}
4. Created a service provider app/lib/CustomSentryServiceProvider
use Cartalyst\Sentry\SentryServiceProvider
class CustomSentryServiceProvider extends SentryServiceProvider{
//What should i put it here?
}
5. Register the service provider in app/config/app.php
'CustomSentryServiceProvider'
6. Use it as :
Sentry::login($credentials);
Well i figured out the answer to the question.
The better way to extend the class would be to use the same name. Since i am already using namespacing it would help reduce the confusion.
Assuming my apps name is MyApp i will only need to replace:
Cartalyst\Sentry
by:
MyApp\Cartalyst\Sentry
the rest of the SentryService provider can be copied as it is.
Then i can call the login method the normal way
Sentry::login($credentials)
I have doubts, because you have to copy whole package/Sentry. Did you tried to extend the Sentry class? This this the best way to do what you want to achieve.