Skip to content

S7k/non tenant side #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ packages:
tutorials:
- tutorial-basic-setup
- tutorial-multi-database
- tutorial-non-tenant
- name: Hyn
description: >
Run multiple websites using the same Laravel installation while
Expand Down
161 changes: 161 additions & 0 deletions docs/tenancy/1.x/tutorial-non-tenant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
title: Non-Tenant Site Setup
icon: fal fa-gears
excerpt: >
How to setup goabal and admin sites in the same application
tags:
- tenancy
- tutorial
- non-tenant
---

## Introduction
When building a SaaS there tends to be a need for a "sub-site" such as an admin panel, or a public facing website describing your SaaS.

This tutorial will go though the basics of setting these "sub-sites" within the same codebase without the need for creating a tenant do display these sites.

## Requirements
This tutorial will assume that you have followed the [basic setup tutorial](tutorial-basic-setup).

## Routes
The first thing we want to do is configure the [Routes Affect](affacts-routes).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The first thing we want to do is configure the [Routes Affect](affacts-routes).
The first thing we want to do is configure the [Routes Affect](affects-routes).


First start by creating a new file for all tenant specific routes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is repeated from above. First we do, first we do that.

A more fluid way of saying this is something like

Suggested change
First start by creating a new file for all tenant specific routes.
In order to set up the tenant specific routes, we will need to create a routes file for it.


### Tenant Route File
For this example it will be stored at `routes\tenant.php`
```php
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Tenant Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your tenant application.
| They are not loaded by default, and will be loaded thought the Routes Affect.
|
*/

Route::get('/', function () {
//This is the "root" route for all tenants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd never use a callable in an example for routes. Callables cause routes files to be no longer cacheable. So.. either just remove the non-working example or provide a controlller from the get-go.

});

```

### Routes Affect Listener
Next we need to create the listener for the [Routes Affect]() that will get rid of the system (non-tenant) routes and load our tenant route file instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link to the right page please


`app\Listeners\ConfigureTenantRoutes.php`
```php
namespace App\Listeners;

use Tenancy\Affects\Routes\Events\ConfigureRoutes;

class TenantRoutes
{
public function handle(ConfigureRoutes $event)
{
if($event->event->tenant)
{
$event
->flush()
->fromFile(
['middleware' => ['web']],
base_path('/routes/tenant.php')
);
}
}
}
```

#### Registering the listener
Now update your `app\Providers\EventServiceProvider.php` to include your new listener.

```php
protected $listen = [

...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...
// ...


Tenancy\Affects\Routes\Events\ConfigureRoutes::class => [
App\Listeners\ConfigureTenantRoutes::class,
]

...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...
// ...

];
```

## Status

At this point you application is configured to load any tenant routes when a tenant is identified. When a tenant is not identified, the routes located in `routes/web.php` will be used instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
At this point you application is configured to load any tenant routes when a tenant is identified. When a tenant is not identified, the routes located in `routes/web.php` will be used instead.
At this point your application is configured to load any tenant routes when a tenant is identified. When a tenant is not identified, the routes located in `routes/web.php` will be used instead.


## Tips and Reminders
### Namespaceing
As with any Laravel application; if you are storing controllers in a different location than in `app\Http\Controllers` you will need to use the namespace option within your routes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this line and the usage of namespace, please. I don't personally recommend using namespaces, rather classes to reference the controllers


`routes\tenant.php`
```php
Route::namespace('Tenant')->group(function () {
// Controllers Within The "App\Http\Controllers\Tenant" Namespace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to provide commented examples than commented explanations..

});
```

### Sub-site by domains
When using subdomains to identify your non-tenant sub-sites you will need to use the route option within your routes file.
`routes\web.php`
```php
Route::domain('admin.myapp.com')->group(function () {
Route::get('/', function () {
// This is the root route for the admin site
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No callables please.

});
});

Route::domain('www.myapp.com')->group(function () {
Route::get('/', function () {
// This is the root route for the public site
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No callables.

});
});

Route::get('/shared', function() {
//This is a route that is shared with both the admin site and the public site
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No callables.

})
```

### Accessing `OnTenant` models from a "sub-site"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename "sub-site" to tenant site or something else? And make it consistent in the whole tutorial.

Before a model that uses the `OnTenant` trait can be used a tenant needs to be identified. Because of this it is best practice to avoid using tenant models on a non-tenant site.

In events where this access is still needed, a tenant will first need to be identified within your code.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could need it, or not, please try to make it look more like an option. Maybe showcase both the ConnectionResolver way and the setting tenant way

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't even think this warning is necessary. Especially on the tenant website you will need to know who the tenant is.. In fact I would even enable eager tenant identification for this.


The following example assumes you are trying to create an admin view that lists all products a specific tenant has in their store.

`app\Http\Controllers\TenantProducts.php`
```php
public function index(Request $request)
{
// We currently do not have a tenant identified as this is the "admin sub-site"

// Get our tenant (A Customer)
$customer = App\Models\Customer::firstOrFail($request->get('tenant_id'));
// Tell tenancy, that we want to access the above tenant
Tenancy::setTenant($customer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend using the ConnectionResolver in order to set the connection, rather than completely setting the tenant as per performance, security etc.


// We are still in the "admin sub-site", but we now also have an active tenant
// And we can access the information stored in their database.

// We will assume that Products uses the `OnTenant` Trait
$tenant_products = Products::all();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

``$tenantProducts` please :D


// Return the products that belong to the tenant still within our "admin sub-site"
return $tenant_products

}
```

## Considerations to take into account
1. **Storing users in tenant databases**
When the User Model has the `OnTenant` trait applied to it, you will not be able to have those users login to any "non-tenant" routes. Because there is no tenant identified, it will not be possible to authenticate the user.
To overcome this you will need to either create two different User models (Admin & User), or will need to store all users in the system database. The option you choose will depend on youre goals and objectives.
Comment on lines +156 to +157
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true and reminds me a lot about the restrictions that hyn/multi-tenant had. If you would want an admin dashboard that allows authenticating users from a tenant, you can create a log in page that asks for their credentials and one more field with a slug, unique hash or something else to identify the tenant with. Then, modify the authentication layer to see whether the identifier is in the payload and set up a tenant database connection on the fly..

t/t is built to be as flexible as possible, the only limitation is your own imagination.


2. **Subdomain identification**
When using subdomain identification, it is best practice to preform an additional check when registering a new tenant that they do not try to use a subdomain you have configured for a "non-tenant site".
I.E. Do not allow users to register the subdomains (`www`, `admin`, etc...)
Comment on lines +160 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without providing an actual implementation for this, I'm not sure this advice is appreciated. Even mentioning this might sound condescending because it seems pretty obvious you want this.