3.x redesign

This commit is contained in:
Samuel Štancl 2020-06-08 21:20:15 +02:00
parent 857122540f
commit f8f354c323
229 changed files with 201175 additions and 22440 deletions

11
source/404.blade.php Normal file
View file

@ -0,0 +1,11 @@
@extends('_layouts.master')
@section('content')
<div class="flex flex-col items-center mt-32 text-gray-800">
<h1 class="text-6xl font-light leading-none mb-2">404</h1>
<h2 class="text-3xl">Page not found</h2>
<hr class="block w-full max-w-sm mx-auto my-8 border">
</div>
@endsection

View file

@ -0,0 +1,102 @@
body {
font-size: 17px;
}
.markdown {
a {
@apply font-semibold text-indigo-700 no-underline;
&:hover {
@apply text-indigo-500;
}
}
blockquote {
@apply pl-6 my-8 text-lg italic font-normal text-gray-800 border-l-4 border-blue-100;
}
code {
@apply px-2 py-px text-sm bg-gray-200 rounded;
}
pre {
@apply p-4 my-4 text-base leading-loose bg-gray-900 rounded;
code {
@apply block p-0 bg-transparent;
}
}
code.hljs {
@apply p-0 bg-transparent;
.hljs-comment,
.hljs-keyword,
.hljs-meta {
@apply font-normal;
/* @apply roman; */
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply mt-8 mb-4 leading-tight text-black;
&:first-child {
@apply mt-0;
}
}
h1 {
@apply text-5xl font-extrabold;
}
h2 {
@apply text-4xl font-bold;
}
h3 {
@apply text-3xl font-bold;
}
h4 {
@apply text-2xl font-normal;
}
h5 {
@apply text-xl font-normal;
}
h6 {
@apply text-lg font-light;
}
hr {
@apply my-12 border-b border-blue-200 rounded-full;
}
li {
@apply ml-5 leading-loose;
ul,
ol {
@apply my-0;
}
}
ol,
ul {
@apply my-4 list-disc;
}
p {
@apply my-6;
}
}

View file

@ -0,0 +1,79 @@
/* Dracula Theme v1.2.5
*
* https://github.com/dracula/highlightjs
*
* Copyright 2016-present, All rights reserved
*
* Code licensed under the MIT license
*
* @author Denis Ciccale <dciccale@gmail.com>
* @author Zeno Rocha <hi@zenorocha.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #282a36;
}
.hljs-built_in,
.hljs-selector-tag,
.hljs-section,
.hljs-link {
color: #8be9fd;
}
.hljs-keyword {
color: #ff79c6;
}
.hljs,
.hljs-subst {
color: #f8f8f2;
}
.hljs-title {
color: #50fa7b;
}
.hljs-string,
.hljs-meta,
.hljs-name,
.hljs-type,
.hljs-attr,
.hljs-symbol,
.hljs-bullet,
.hljs-addition,
.hljs-variable,
.hljs-template-tag,
.hljs-template-variable {
color: #f1fa8c;
}
.hljs-comment,
.hljs-quote,
.hljs-deletion {
color: #6272a4;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-title,
.hljs-section,
.hljs-doctag,
.hljs-type,
.hljs-name,
.hljs-strong {
font-weight: bold;
}
.hljs-literal,
.hljs-number {
color: #bd93f9;
}
.hljs-emphasis {
font-style: italic;
}

View file

@ -0,0 +1,99 @@
/* a11y-light theme */
/* Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css */
/* @author: ericwbailey */
/* Comment */
.hljs-comment,
.hljs-quote {
color: #696969;
}
/* Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #d91e18;
}
/* Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #aa5d00;
}
/* Yellow */
.hljs-attribute {
color: #aa5d00;
}
/* Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #008000;
}
/* Blue */
.hljs-title,
.hljs-section {
color: #007faa;
}
/* Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #7928a1;
}
.hljs {
display: block;
overflow-x: auto;
background: #fefefe;
color: #545454;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
@media screen and (-ms-high-contrast: active) {
.hljs-addition,
.hljs-attribute,
.hljs-built_in,
.hljs-builtin-name,
.hljs-bullet,
.hljs-comment,
.hljs-link,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-params,
.hljs-string,
.hljs-symbol,
.hljs-type,
.hljs-quote {
color: highlight;
}
.hljs-keyword,
.hljs-selector-tag {
font-weight: bold;
}
}

View file

@ -0,0 +1,32 @@
.nav-menu {
@apply mb-4;
@screen lg {
@apply block pr-4 bg-transparent border-b-0 shadow-none;
}
}
.nav-menu__item {
@apply block mt-4 text-base font-normal leading-loose tracking-wide text-gray-600 no-underline uppercase;
}
a.nav-menu__item {
@apply inline mt-0 font-medium tracking-normal text-indigo-800 normal-case;
}
a.nav-menu__item:hover {
@apply text-indigo-600;
}
.nav-menu .active {
@apply font-semibold text-indigo-600;
}
ul.list-reset ul.list-reset {
@apply pl-4;
}
.turbolinks-progress-bar {
@apply bg-indigo-500;
height: 3px;
}

View file

@ -0,0 +1,77 @@
/*
.docsearch-input {
background-image: url('assets/img/magnifying-glass.svg');
background-position: 0.8em;
background-repeat: no-repeat;
text-indent: 1.2em;
@screen lg {
&:focus {
@apply w-2/3;
}
}
@screen xl {
&:focus {
@apply w-3/4;
}
}
&__wrapper {
@apply absolute top-0 left-0 z-10 w-full px-4 mt-8 bg-white;
@screen md {
@apply relative px-0 mt-0;
}
}
}*/
.algolia-autocomplete {
@apply w-full text-right;
input#docsearch:focus {
@apply border-2 border-indigo-500 outline-none;
}
.ds-dropdown-menu {
@apply w-full;
max-width: 750px !important;
min-width: auto !important;
.algolia-docsearch-suggestion {
.algolia-docsearch-suggestion--content {
@apply w-full;
@screen md {
@apply w-2/3;
}
}
.algolia-docsearch-suggestion--text {
@apply font-normal;
line-height: 1.4;
}
.algolia-docsearch-suggestion--wrapper {
@apply py-3;
}
.algolia-docsearch-suggestion--subcategory-column {
@apply hidden;
@screen md {
@apply inline-block w-1/3;
}
}
}
}
}
.algolia-autocomplete .algolia-docsearch-suggestion--highlight {
@apply text-indigo-700;
}

View file

@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
/* @import 'highlight'; */
@import 'dracula';
@import 'base';
@import 'navigation';
@import 'search';
@tailwind utilities;

15
source/_assets/js/main.js Normal file
View file

@ -0,0 +1,15 @@
import hljs from 'highlight.js/lib/highlight';
hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash'));
hljs.registerLanguage('css', require('highlight.js/lib/languages/css'));
hljs.registerLanguage('html', require('highlight.js/lib/languages/xml'));
hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'));
hljs.registerLanguage('json', require('highlight.js/lib/languages/json'));
hljs.registerLanguage('markdown', require('highlight.js/lib/languages/markdown'));
hljs.registerLanguage('php', require('highlight.js/lib/languages/php'));
hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss'));
hljs.registerLanguage('yaml', require('highlight.js/lib/languages/yaml'));
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});

View file

@ -0,0 +1,10 @@
var Turbolinks = require("turbolinks")
Turbolinks.start();
// make progressbar always visible (default 500)
//Turbolinks.setProgressBarDelay(0);
// document.addEventListener("turbolinks:load", function() {
// console.log('turbolinks ready');
// })

View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="description" content="{{ $page->description ?? $page->siteDescription }}">
<meta property="og:site_name" content="{{ $page->siteName }}"/>
<meta property="og:title" content="{{ $page->title ? $page->title . ' | ' : '' }}{{ $page->siteName }}"/>
<meta property="og:description" content="{{ $page->description ?? $page->title }} | {{ $page->siteName }}"/>
<meta property="og:url" content="{{ $page->getUrl() }}"/>
<meta property="og:image" content="/assets/img/logo.png"/>
<meta property="og:type" content="website"/>
<meta name="theme-color" content="#5850EC">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="twitter:image:alt" content="{{ $page->siteName }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="docsearch:language" content="en" />
<meta name="docsearch:version" content="{{ $page->version() }}" />
@if ($page->docsearchApiKey && $page->docsearchIndexName)
<meta name="generator" content="tighten_jigsaw_doc">
@endif
<title>{{ $page->title ? $page->title . ' | ' : '' }}{{ $page->siteName }}</title>
<link rel="home" href="{{ $page->baseUrl }}">
@stack('meta')
@if ($page->production)
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-168455954-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-168455954-1');
</script>
@endif
<link href="https://fonts.googleapis.com/css?family=Fira+Code&display=swap" rel="stylesheet">
<link href="https://rsms.me/inter/inter.css" rel="stylesheet">
<link rel="stylesheet" href="{{ mix('css/main.css', 'assets/build') }}">
<script src="{{ mix('js/turbolinks.js', 'assets/build') }}"></script>
@if ($page->docsearchApiKey && $page->docsearchIndexName)
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
@endif
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" rel="stylesheet">
</head>
<body class="font-sans antialiased">
<div class="p-2">
@include('_partials.header-docs')
<main role="main" class="">
<section class="px-6 py-4">
<div class="grid grid-cols-8 gap-4">
<nav id="js-nav-menu" class="nav-menu md:block md:col-span-2 hidden col-span-8 font-semibold text-indigo-700">
@include('_nav.menu', ['items' => $page->navigation[$page->version()]])
</nav>
<div class="markdown lg:pl-4 md:col-span-6 lg:col-span-4 col-span-8 pb-16 break-words" v-pre>
@yield('content')
</div>
</div>
</section>
</main>
</div>
@include('_partials.footer')
@if ($page->docsearchApiKey && $page->docsearchIndexName)
<script src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js" data-turbolinks-eval="false"></script>
@endif
<script src="{{ $page->baseUrl . mix('js/main.js', 'assets/build') }}"></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.3.5/dist/alpine.min.js" defer></script>
@stack('scripts')
</body>
</html>

View file

@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta property="og:site_name" content="{{ $page->siteName }}"/>
<meta property="og:title" content="Automatic & flexible multi-tenancy package for Laravel."/>
<meta property="og:description" content="{{ $page->siteDescription }}"/>
<meta property="og:image" content="/assets/img/logo.png"/>
<meta property="og:type" content="website"/>
<meta name="theme-color" content="#5850EC">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="{{ mix('css/main.css', 'assets/build') }}">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
<title>{{ ($title ?? null) ? $title . ' | Tenancy for Laravel' : 'Tenancy for Laravel' }}</title>
</head>
<body class="font-sans antialiased">
{{-- Header --}}
@include('_partials.header')
{{-- /Header --}}
<main class="
{{ $page->centered ? 'p-8 md:w-1/2 w-full mx-auto' : '' }}
{{ $page->markdown ? 'markdown' : '' }}
">
@yield('content')
</main>
{{-- Footer --}}
@include('_partials.footer')
{{-- /Footer --}}
<script src="{{ $page->baseUrl . mix('js/main.js', 'assets/build') }}"></script>
</body>
</html>

View file

@ -0,0 +1,17 @@
<li class="list-reset">
@if ($url = is_string($item) ? $item : $item->url)
<a href="{{ $page->isUrl($url) ? $url : $page->link($url) }}"
class="{{ 'lvl' . $level }} {{ $page->isActiveParent($item) ? 'lvl' . $level . '-active' : '' }} {{ $page->isActive($url) ? 'active' : '' }} nav-menu__item"
>
{{ $label }}
</a>
@else
{{-- Menu item without URL--}}
<p class="nav-menu__item text-grey-dark">{{ $label }}</p>
@endif
@if (! is_string($item) && $item->children)
{{-- Recursively handle children --}}
@include('_nav.menu', ['items' => $item->children, 'level' => ++$level])
@endif
</li>

View file

@ -0,0 +1,7 @@
@php $level = $level ?? 0 @endphp
<ul class="my-0 list-reset">
@foreach ($items as $label => $item)
@include('_nav.menu-item')
@endforeach
</ul>

View file

@ -0,0 +1,49 @@
{{-- <button
title="Start searching"
type="button"
class="md:hidden bg-grey-lightest hover:bg-blue-lightest border-grey focus:outline-none flex items-center justify-center h-10 px-3 border rounded-full"
onclick="searchInput.toggle()"
>
<img src="/docs/assets/img/magnifying-glass.svg" alt="search icon" class="max-w-none w-4 h-4">
</button> --}}
<div id="js-search-input" class="lg:px-6 lg:w-3/4 xl:px-12 flex-grow w-full">
<div class=" relative rounded-md">
<span class="algolia-autocomplete algolia-autocomplete-right" style="position: relative; display: inline-block; direction: ltr;"><input id="docsearch" class="focus:outline-0 ds-input block w-full py-2 pl-10 pr-4 leading-normal placeholder-gray-600 transition-colors duration-100 ease-in-out bg-gray-100 border border-transparent rounded-md" type="text" placeholder="Search the documentation" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-label="search input" aria-owns="algolia-autocomplete-listbox-0" style="position: relative; vertical-align: top;" dir="auto"><pre aria-hidden="true" style="position: absolute; visibility: hidden; white-space: pre; font-family: Inter var, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400; word-spacing: 0px; letter-spacing: normal; text-indent: 0px; text-rendering: optimizelegibility; text-transform: none;">min</pre><span class="ds-dropdown-menu ds-with-1" style="position: absolute; top: 100%; z-index: 100; left: 0px; right: auto; display: none;" role="listbox" id="algolia-autocomplete-listbox-0"><div class="ds-dataset-1"></div></span></span>
<div class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
<svg class="w-4 h-4 text-gray-600 pointer-events-none fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"></path></svg>
</div>
</div>
</div>
{{--
<div id="js-search-input" class="docsearch-input__wrapper md:block">
<label for="search" class="hidden">Search</label>
<input
id="docsearch-input"
class="docsearch-input transition-fast lg:w-3/4 bg-grey-lightest text-grey-darker border-grey focus:border-blue-light relative block w-full h-10 px-4 pb-0 mx-auto border rounded-full outline-none"
name="docsearch"
type="text"
placeholder="Search"
>
<button
class="md:hidden pin-t pin-r text-blue hover:text-blue-dark focus:outline-none pr-7 absolute h-full -mt-px text-3xl font-light"
onclick="searchInput.toggle()"
>&times;</button>
</div>--}}
@push('scripts')
@if ($page->docsearchApiKey && $page->docsearchIndexName)
<script type="text/javascript">
docsearch({
apiKey: '{{ $page->docsearchApiKey }}',
indexName: '{{ $page->docsearchIndexName }}',
algoliaOptions: { 'facetFilters': ["version:{{ $page->version() }}", "language:en"] },
inputSelector: '#docsearch',
debug: false // Set debug to true if you want to inspect the dropdown
});
</script>
@endif
@endpush

View file

@ -0,0 +1,141 @@
<div class="bg-gray-900">
<div class="max-w-screen-xl px-4 py-12 mx-auto sm:px-6 lg:py-16 lg:px-8">
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
<div class="grid grid-cols-2 gap-8 xl:col-span-2">
<div class="md:grid md:grid-cols-2 md:gap-8">
<div>
<h4 class="text-sm font-semibold leading-5 tracking-wider text-gray-400 uppercase">
Documentation
</h4>
<ul class="mt-4">
<li>
<a href="/docs/v3/tenants" class="text-base leading-6 text-gray-300 hover:text-white">
Tenants
</a>
</li>
<li class="mt-4">
<a href="/docs/v3/event-system" class="text-base leading-6 text-gray-300 hover:text-white">
Event system
</a>
</li>
<li class="mt-4">
<a href="/docs/v3/configuration" class="text-base leading-6 text-gray-300 hover:text-white">
Configuration
</a>
</li>
</ul>
</div>
<div class="mt-12 md:mt-0">
<h4 class="text-sm font-semibold leading-5 tracking-wider text-gray-400 uppercase">
Documentation
</h4>
<ul class="mt-4">
<li class="mt-4">
<a href="/docs/v3/package-comparison" class="text-base leading-6 text-gray-300 hover:text-white">
Compared to other packages
</a>
</li>
<li class="mt-4">
<a href="/docs/v3/integrating" class="text-base leading-6 text-gray-300 hover:text-white">
Integrations
</a>
</li>
<li class="mt-4">
<a href="/docs/v3/tenant-identification" class="text-base leading-6 text-gray-300 hover:text-white">
Tenant identification
</a>
</li>
</ul>
</div>
</div>
<div class="md:grid md:grid-cols-2 md:gap-8">
<div class="">
<h4 class="text-sm font-semibold leading-5 tracking-wider text-gray-400 uppercase">
Business
</h4>
<ul class="mt-4">
<li>
<a href="/saas-boilerplate" class="text-base leading-6 text-gray-300 hover:text-white">
SaaS boilerplate
</a>
</li>
<li class="mt-4">
<a href="/contact" class="text-base leading-6 text-gray-300 hover:text-white">
Consulting
</a>
</li>
<li class="mt-4">
<a href="/contact" class="text-base leading-6 text-gray-300 hover:text-white">
Audits
</a>
</li>
</ul>
</div>
<div class="mt-12 md:mt-0">
<h4 class="text-sm font-semibold leading-5 tracking-wider text-gray-400 uppercase">
Links
</h4>
<ul class="mt-4">
<li>
<a href="/branding" class="text-base leading-6 text-gray-300 hover:text-white">
Branding
</a>
</li>
<li class="mt-4">
<a href="https://github.com/stancl/tenancy" class="text-base leading-6 text-gray-300 hover:text-white">
GitHub
</a>
</li>
<li class="mt-4">
<a href="https://discord.gg/7cpgPxv" class="text-base leading-6 text-gray-300 hover:text-white">
Discord
</a>
</li>
<li class="mt-4">
<a href="/donate" class="text-base leading-6 text-gray-300 hover:text-white">
Donate
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="mt-8 xl:mt-0">
<h4 class="text-sm font-semibold leading-5 tracking-wider text-gray-400 uppercase">
Subscribe to our newsletter
</h4>
<p class="mt-4 text-base leading-6 text-gray-300">
Receive notifications about important releases, new packages and other updates.
</p>
<form action="https://github.us3.list-manage.com/subscribe/post?u=6a33c422777aedd88e9a9488e&amp;id=9b99f013b8" method="post" target="_blank" class="mt-4 sm:flex sm:max-w-md">
<input name="EMAIL" aria-label="Email address" type="email" required class="w-full px-5 py-3 text-base leading-6 text-gray-900 placeholder-gray-500 transition duration-150 ease-in-out bg-white border border-transparent rounded-md appearance-none focus:outline-none focus:placeholder-gray-400" placeholder="Enter your email" />
<input type="hidden" value="8" name="group[27425][8]" id="mce-group[27425]-27425-3">
<div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3 sm:flex-shrink-0">
<button class="flex items-center justify-center w-full px-5 py-3 text-base font-medium leading-6 text-white transition duration-150 ease-in-out bg-indigo-600 border border-transparent rounded-md hover:bg-indigo-500 focus:outline-none focus:bg-indigo-500">
Subscribe
</button>
</div>
</form>
</div>
</div>
<div class="pt-8 mt-8 border-t border-gray-700 md:flex md:items-center md:justify-between">
<div class="flex md:order-2">
<a href="https://twitter.com/samuelstancl" class="ml-6 text-gray-400 hover:text-gray-300">
<span class="sr-only">Twitter</span>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
</svg>
</a>
<a href="https://github.com/stancl/tenancy" class="ml-6 text-gray-400 hover:text-gray-300">
<span class="sr-only">GitHub</span>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
</svg>
</a>
</div>
<p class="mt-8 text-base leading-6 text-gray-400 md:mt-0 md:order-1">
&copy; 2020 Samuel Štancl. All rights reserved.
</p>
</div>
</div>
</div>

View file

@ -0,0 +1,83 @@
<div x-data="navMenu()" class="relative bg-white" data-turbolinks-permanent>
<div class="sm:px-6 md:justify-start md:space-x-10 flex items-center justify-between px-4 pt-3 pb-6">
<div class="lg:w-0 lg:flex-1">
<a href="{{ $page->baseUrl }}" class="flex" data-turbolinks="false">
<img src="/assets/img/tenancyforlaravel.svg" alt="" style="height: 70px">
</a>
</div>
<div class="md:hidden -my-2 -mr-2">
<button @click="toggle()" type="button"
class="hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 inline-flex items-center justify-center p-2 text-gray-400 transition duration-150 ease-in-out rounded-md">
<svg class="w-6 h-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<nav class="md:flex flex-grow hidden">
@include('_nav.search-input')
</nav>
<div class="md:flex md:flex-1 lg:w-0 items-center justify-end hidden space-x-8">
<div x-data="{ flyoutMenuOpen: false }" @click.away="flyoutMenuOpen = false" class="relative">
<button type="button" @click="flyoutMenuOpen = !flyoutMenuOpen" x-state:on="Item active"
x-state:off="Item inactive" :class="{ 'text-gray-900': flyoutMenuOpen, 'text-gray-500': !flyoutMenuOpen }"
class="group hover:text-gray-900 focus:outline-none focus:text-gray-900 inline-flex items-center space-x-2 text-base font-medium leading-6 text-gray-500 transition duration-150 ease-in-out">
<span>Version {{ $page->versions[$page->version()] }}</span>
<svg x-state-on="Item active" x-state:on="Item active" x-state-off="Item inactive" x-state:off="Item inactive"
class="group-hover:text-gray-500 group-focus:text-gray-500 w-5 h-5 text-gray-400 transition duration-150 ease-in-out"
:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }"
x-bind-class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }" fill="currentColor"
viewBox="0 0 20 20" null="[object Object]">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
<div x-description="'More' flyout menu, show/hide based on flyout menu state." x-show="flyoutMenuOpen"
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-1"
class="left-1/2 sm:px-0 absolute w-screen max-w-xs px-2 mt-6 transform -translate-x-1/2"
style="display: none; max-width: 10rem;">
<div class="rounded-lg shadow-lg">
<div class="overflow-hidden rounded-lg shadow-xs">
<div class="sm:gap-2 sm:p-2 relative z-20 grid gap-2 px-5 py-6 bg-white">
@foreach ($page->versions as $version => $name)
<a data-turbolinks="false" href="{{ $page->baseUrl . "/docs/" . $version }}"
class="hover:bg-gray-50 block p-3 space-y-1 transition duration-150 ease-in-out rounded-md">
<p class="my-0 text-base font-medium leading-6 text-gray-900">
Version {{ $name }}
</p>
</a>
@endforeach
</div>
</div>
</div>
</div>
</div>
<span class="inline-flex rounded-md shadow-sm">
<a href="{{ $page->githubUrl }}" data-turbolinks="false"
class="hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 inline-flex items-center justify-center px-4 py-2 text-base font-medium leading-6 text-white whitespace-no-wrap transition duration-150 ease-in-out bg-indigo-600 border border-transparent rounded-md">
GitHub
</a>
</span>
</div>
</div>
</div>
@push('scripts')
<script>
function navMenu() {
return {
mobileMenuOpen: false,
toggle() {
const menu = document.getElementById('js-nav-menu');
menu.classList.toggle('hidden');
menu.classList.toggle('lg:block');
mobileMenuOpen = !mobileMenuOpen;
},
}
}
</script>
@endpush

View file

@ -0,0 +1,303 @@
<div x-data="{ mobileMenuOpen: false }" class="relative z-10 bg-white">
<div class="sm:px-6 md:justify-start md:space-x-10 flex items-center justify-between px-4 pt-3 pb-6">
<div class="lg:w-0 lg:flex-1">
<a href="{{ $page->baseUrl }}" class="flex">
<img src="/assets/img/tenancyforlaravel.svg" alt="Tenancy for Laravel" style="height: 70px">
</a>
</div>
<div class="md:hidden -my-2 -mr-2">
<button @click="mobileMenuOpen = true" type="button" class="hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 inline-flex items-center justify-center p-2 text-gray-400 transition duration-150 ease-in-out rounded-md">
<svg class="w-6 h-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<nav class="md:flex hidden space-x-10">
<div @click.away="flyoutMenuOpen = false" x-data="{ flyoutMenuOpen: false }" class="relative">
<button type="button" @click="flyoutMenuOpen = !flyoutMenuOpen" x-state:on="Item active" x-state:off="Item inactive" :class="{ 'text-gray-900': flyoutMenuOpen, 'text-gray-500': !flyoutMenuOpen }" class="group hover:text-gray-900 focus:outline-none focus:text-gray-900 inline-flex items-center space-x-2 text-base font-medium leading-6 text-gray-500 transition duration-150 ease-in-out">
<span>Documentation
</span>
<svg x-state:on="Item active" x-state:off="Item inactive" class="group-hover:text-gray-500 group-focus:text-gray-500 w-5 h-5 text-gray-400 transition duration-150 ease-in-out" x-bind:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
<div x-description="'Documentation' flyout menu, show/hide based on flyout menu state." x-show="flyoutMenuOpen" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-1" class="md:max-w-3xl lg:ml-0 lg:left-1/2 lg:-translate-x-1/2 absolute w-screen max-w-md mt-3 -ml-4 transform" style="display: none;">
<div class="rounded-lg shadow-lg">
<div class="overflow-hidden rounded-lg shadow-xs">
<div class="sm:gap-8 sm:p-8 lg:grid-cols-2 relative z-20 grid gap-6 px-5 py-6 bg-white">
<a href="/docs/v3/tenants" class="hover:bg-gray-50 flex items-start p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="sm:h-12 sm:w-12 flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
</div>
<div class="space-y-1">
<p class="text-base font-medium leading-6 text-gray-900">
Tenants
</p>
<p class="text-sm leading-5 text-gray-500">
Everything about the Tenant model. Creating tenants, customizing behavior and more.
</p>
</div>
</a>
<a href="/docs/v3/package-comparison" class="hover:bg-gray-50 flex items-start p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="sm:h-12 sm:w-12 flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"></path>
</svg>
</div>
<div class="space-y-1">
<p class="text-base font-medium leading-6 text-gray-900">
Compared to other packages
</p>
<p class="text-sm leading-5 text-gray-500">
This package compared to other Laravel multi-tenancy packages.
</p>
</div>
</a>
<a href="/docs/v3/event-system" class="hover:bg-gray-50 flex items-start p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="sm:h-12 sm:w-12 flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>
</div>
<div class="space-y-1">
<p class="text-base font-medium leading-6 text-gray-900">
Event system
</p>
<p class="text-sm leading-5 text-gray-500">
The glue that holds together the pieces that make up this package.
</p>
</div>
</a>
<a href="/docs/v3/integrating" class="hover:bg-gray-50 flex items-start p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="sm:h-12 sm:w-12 flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"></path>
</svg>
</div>
<div class="space-y-1">
<p class="text-base font-medium leading-6 text-gray-900">
Integrations
</p>
<p class="text-sm leading-5 text-gray-500">
A list of verified integrations with other Laravel packages.
</p>
</div>
</a>
<a href="/docs/v3/configuration" class="hover:bg-gray-50 flex items-start p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="sm:h-12 sm:w-12 flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<div class="space-y-1">
<p class="text-base font-medium leading-6 text-gray-900">
Configuration
</p>
<p class="text-sm leading-5 text-gray-500">
Configure this package to behave exactly the way that fits your needs.
</p>
</div>
</a>
<a href="/docs/v3/tenant-identification" class="hover:bg-gray-50 flex items-start p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="sm:h-12 sm:w-12 flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div class="space-y-1">
<p class="text-base font-medium leading-6 text-gray-900">
Tenant identification
</p>
<p class="text-sm leading-5 text-gray-500">
Everything about identifying tenants. Middlewares, resolvers, or manual identification.
</p>
</div>
</a>
</div>
<div class="bg-gray-50 sm:p-8 p-5">
<a href="/docs/v3" class="hover:bg-gray-100 flow-root p-3 -m-3 space-y-1 transition duration-150 ease-in-out rounded-md">
<div class="flex items-center space-x-3">
<div class="text-base font-medium leading-6 text-gray-900">
Open documentation
</div>
</div>
<p class="text-sm leading-5 text-gray-500">
View the full documentation.
</p>
</a>
</div>
</div>
</div>
</div>
</div>
<div x-data="{ flyoutMenuOpen: false }" @click.away="flyoutMenuOpen = false" class="relative">
<button type="button" @click="flyoutMenuOpen = !flyoutMenuOpen" x-state:on="Item active" x-state:off="Item inactive" :class="{ 'text-gray-900': flyoutMenuOpen, 'text-gray-500': !flyoutMenuOpen }" class="group hover:text-gray-900 focus:outline-none focus:text-gray-900 inline-flex items-center space-x-2 text-base font-medium leading-6 text-gray-500 transition duration-150 ease-in-out">
<span>Business
</span>
<svg x-state:on="Item active" x-state:off="Item inactive" class="group-hover:text-gray-500 group-focus:text-gray-500 w-5 h-5 text-gray-400 transition duration-150 ease-in-out" x-bind:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
<div x-description="'Business' flyout menu, show/hide based on flyout menu state." x-show="flyoutMenuOpen" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-1" class="left-1/2 sm:px-0 absolute w-screen max-w-xs px-2 mt-3 transform -translate-x-1/2" style="display: none;">
<div class="rounded-lg shadow-lg">
<div class="overflow-hidden rounded-lg shadow-xs">
<div class="sm:gap-8 sm:p-8 relative z-20 grid gap-6 px-5 py-6 bg-white">
<a href="/saas-boilerplate" class="hover:bg-gray-50 block p-3 -m-3 space-y-1 transition duration-150 ease-in-out rounded-md">
<p class="text-base font-medium leading-6 text-gray-900">
SaaS boilerplate
</p>
<p class="text-sm leading-5 text-gray-500">
A fully featured Laravel application skeleton with multi-tenancy, tenant signup flow, Cashier billing and a Nova admin panel.
</p>
</a>
<a href="/contact" class="hover:bg-gray-50 block p-3 -m-3 space-y-1 transition duration-150 ease-in-out rounded-md">
<p class="text-base font-medium leading-6 text-gray-900">
Consulting
</p>
<p class="text-sm leading-5 text-gray-500">
We offer consulting services for businesses who need help with implementing our package or related features.
</p>
</a>
<a href="/contact" class="hover:bg-gray-50 block p-3 -m-3 space-y-1 transition duration-150 ease-in-out rounded-md">
<p class="text-base font-medium leading-6 text-gray-900">
Audits
</p>
<p class="text-sm leading-5 text-gray-500">
Have the package author to review your tenancy implementation before you launch.
</p>
</a>
</div>
</div>
</div>
</div>
</div>
<a target="_blank" href="https://github.com/stancl/tenancy" class="hidden xl:inline hover:text-gray-900 focus:outline-none focus:text-gray-900 text-base font-medium leading-6 text-gray-500 transition duration-150 ease-in-out">
GitHub
</a>
<a target="_blank" href="https://discord.gg/7cpgPxv" class="hover:text-gray-900 focus:outline-none focus:text-gray-900 text-base font-medium leading-6 text-gray-500 transition duration-150 ease-in-out">
Discord
</a>
<a href="/donate" class="hover:text-gray-900 focus:outline-none focus:text-gray-900 text-base font-medium leading-6 text-gray-500 transition duration-150 ease-in-out">
Donate
</a>
</nav>
<div class="md:flex md:flex-1 lg:w-0 items-center justify-end hidden space-x-8">
<a href="/docs/v3/quickstart/" class="hover:text-gray-900 focus:outline-none focus:text-gray-900 text-base font-medium leading-6 text-gray-500 whitespace-no-wrap">
Tutorial
</a>
<span class="inline-flex rounded-md shadow-sm">
<a href="/docs/" class="hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 inline-flex items-center justify-center px-4 py-2 text-base font-medium leading-6 text-white whitespace-no-wrap transition duration-150 ease-in-out bg-indigo-600 border border-transparent rounded-md">
Documentation
</a>
</span>
</div>
</div>
<div x-description="Mobile menu, show/hide based on mobile menu state." x-show="mobileMenuOpen" x-transition:enter="duration-200 ease-out" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="duration-100 ease-in" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" class="md:hidden absolute inset-x-0 top-0 p-2 transition origin-top-right transform">
<div class="rounded-lg shadow-lg">
<div class="divide-gray-50 bg-white divide-y-2 rounded-lg shadow-xs">
<div class="px-5 pt-5 pb-6 space-y-6">
<div class="flex items-center justify-between">
<div>
<img src="/assets/img/tenancyforlaravel.svg" alt="Tenancy for Laravel" style="height: 70px">
</div>
<div class="-mr-2">
<button @click="mobileMenuOpen = false" type="button" class="hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 inline-flex items-center justify-center p-2 text-gray-400 transition duration-150 ease-in-out rounded-md">
<svg class="w-6 h-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<div>
<nav class="gap-7 grid grid-cols-1">
<a href="/docs/v3/tenants" class="hover:bg-gray-50 flex items-center p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
</div>
<div class="text-base font-medium leading-6 text-gray-900">
Tenants
</div>
</a>
<a href="/docs/v3/package-comparison" class="hover:bg-gray-50 flex items-center p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"></path>
</svg>
</div>
<div class="text-base font-medium leading-6 text-gray-900">
Compared to other packages
</div>
</a>
<a href="/docs/v3/event-system" class="hover:bg-gray-50 flex items-center p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>
</div>
<div class="text-base font-medium leading-6 text-gray-900">
Event system
</div>
</a>
<a href="/docs/v3/integrating" class="hover:bg-gray-50 flex items-center p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"></path>
</svg>
</div>
<div class="text-base font-medium leading-6 text-gray-900">
Integrations
</div>
</a>
<a href="/docs/v3/configuration" class="hover:bg-gray-50 flex items-center p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</div>
<div class="text-base font-medium leading-6 text-gray-900">
Configuration
</div>
</a>
<a href="/docs/v3/tenant-identification" class="hover:bg-gray-50 flex items-center p-3 -m-3 space-x-4 transition duration-150 ease-in-out rounded-lg">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 text-white bg-indigo-500 rounded-md">
<svg class="w-6 h-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div class="text-base font-medium leading-6 text-gray-900">
Tenant identification
</div>
</a>
</nav>
</div>
</div>
<div class="px-5 py-6 space-y-6">
<div class="grid grid-cols-2 gap-4">
<a href="/saas-boilerplate" class="hover:text-gray-700 text-base font-medium leading-6 text-gray-900 transition duration-150 ease-in-out">
SaaS boilerplate
</a>
<a href="/contact" class="hover:text-gray-700 text-base font-medium leading-6 text-gray-900 transition duration-150 ease-in-out">
Consulting
</a>
<a href="/contact" class="hover:text-gray-700 text-base font-medium leading-6 text-gray-900 transition duration-150 ease-in-out">
Audits
</a>
<a target="_blank" href="https://github.com/stancl/tenancy" class="hover:text-gray-700 text-base font-medium leading-6 text-gray-900 transition duration-150 ease-in-out">
GitHub
</a>
<a target="_blank" href="https://discord.gg/vHjEyrw" class="hover:text-gray-700 text-base font-medium leading-6 text-gray-900 transition duration-150 ease-in-out">
Discord
</a>
<a href="/donate" class="hover:text-gray-700 text-base font-medium leading-6 text-gray-900 transition duration-150 ease-in-out">
Donate
</a>
</div>
<div class="space-y-6">
<span class="flex w-full rounded-md shadow-sm">
<a href="/docs/v3" class="hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 flex items-center justify-center w-full px-4 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out bg-indigo-600 border border-transparent rounded-md">
Documentation
</a>
</span>
<p class="text-base font-medium leading-6 text-center text-gray-500">
<a href="/docs/v3/quickstart" class="hover:text-indigo-500 text-indigo-600 transition duration-150 ease-in-out">
Tutorial
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>

BIN
source/android-chrome-192x192.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
source/android-chrome-512x512.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
source/apple-touch-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,260 @@
body {
font-size: 17px;
}
.markdown body {
font-size: 17px;
}
.markdown a {
font-weight: 600;
--text-opacity: 1;
color: #5145cd;
color: rgba(81, 69, 205, var(--text-opacity));
text-decoration: none
}
.markdown a:hover {
--text-opacity: 1;
color: #6875f5;
color: rgba(104, 117, 245, var(--text-opacity));
}
.markdown blockquote {
padding-left: 1.5rem;
margin-top: 2rem;
margin-bottom: 2rem;
font-size: 1.125rem;
font-style: italic;
font-weight: 400;
--text-opacity: 1;
color: #252f3f;
color: rgba(37, 47, 63, var(--text-opacity));
border-left-width: 4px;
--border-opacity: 1;
border-color: #e1effe;
border-color: rgba(225, 239, 254, var(--border-opacity));
}
.markdown code {
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 1px;
padding-bottom: 1px;
font-size: 0.875rem;
--bg-opacity: 1;
background-color: #e5e7eb;
background-color: rgba(229, 231, 235, var(--bg-opacity));
border-radius: 0.25rem;
}
.markdown code.hljs {
padding: 0;
background-color: transparent;
}
.markdown code.hljs .hljs-comment,
.markdown code.hljs .hljs-keyword,
.markdown code.hljs .hljs-meta {
font-weight: 400;
/* @apply roman; */
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
margin-top: 2rem;
margin-bottom: 1rem;
line-height: 1.25;
--text-opacity: 1;
color: #000000;
color: rgba(0, 0, 0, var(--text-opacity))
}
.markdown h1:first-child, .markdown h2:first-child, .markdown h3:first-child, .markdown h4:first-child, .markdown h5:first-child, .markdown h6:first-child {
margin-top: 0;
}
.markdown h1 {
font-size: 3rem;
font-weight: 800;
}
.markdown h2 {
font-size: 2.25rem;
font-weight: 700;
}
.markdown h3 {
font-size: 1.875rem;
font-weight: 700;
}
.markdown h4 {
font-size: 1.5rem;
font-weight: 400;
}
.markdown h5 {
font-size: 1.25rem;
font-weight: 400;
}
.markdown h6 {
font-size: 1.125rem;
font-weight: 300;
}
.markdown hr {
margin-top: 3rem;
margin-bottom: 3rem;
border-bottom-width: 1px;
--border-opacity: 1;
border-color: #c3ddfd;
border-color: rgba(195, 221, 253, var(--border-opacity));
border-radius: 9999px;
}
.markdown li ul,
.markdown li ol {
margin-top: 0;
margin-bottom: 0;
}
.markdown ol,
.markdown ul {
margin-top: 1rem;
margin-bottom: 1rem;
}
.markdown p {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.markdown pre {
padding: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
font-size: 1rem;
line-height: 2;
--bg-opacity: 1;
background-color: #f4f5f7;
background-color: rgba(244, 245, 247, var(--bg-opacity));
border-radius: 0.25rem;
}
.markdown pre code {
display: block;
padding: 0;
background-color: transparent;
}
.nav-menu {
margin-bottom: 1rem;
@media (min-width: 1024px) {
display: block;
padding-right: 1rem;
background-color: transparent;
border-bottom-width: 0;
box-shadow: none;
}
}
.nav-menu .active {
--text-opacity: 1;
color: #000000;
color: rgba(0, 0, 0, var(--text-opacity));
}
.nav-menu__item {
display: block;
margin-top: 0;
margin-bottom: 0.5rem;
font-size: 0.875rem;
--text-opacity: 1;
color: #5145cd;
color: rgba(81, 69, 205, var(--text-opacity));
text-decoration: none;
/* @apply .list-reset; ??? */
}
ul.list-reset ul.list-reset {
padding-left: 1rem;
}
.turbolinks-progress-bar {
--bg-opacity: 1;
background-color: #6875f5;
background-color: rgba(104, 117, 245, var(--bg-opacity));
height: 3px;
}
.docsearch-input {
background-image: url('assets/img/magnifying-glass.svg');
background-position: 0.8em;
background-repeat: no-repeat;
text-indent: 1.2em;
@media (min-width: 1024px) {
&:focus {
width: 66.666667%;
}
}
@media (min-width: 1280px) {
&:focus {
width: 75%;
}
}
/* &__wrapper {
@apply absolute top-0 left-0 z-10 w-full px-4 mt-8 bg-white;
@screen md {
@apply relative px-0 mt-0;
}
} */
}
.algolia-autocomplete {
width: 100%;
text-align: right;
}
.algolia-autocomplete .ds-dropdown-menu {
width: 100%;
max-width: 750px !important;
min-width: auto !important;
}
.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion .algolia-docsearch-suggestion--content {
width: 100%;
@media (min-width: 768px) {
width: 66.666667%;
}
}
.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion .algolia-docsearch-suggestion--text {
font-weight: 400;
line-height: 1.4;
}
.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion .algolia-docsearch-suggestion--wrapper {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.algolia-autocomplete .ds-dropdown-menu .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column {
display: none;
@media (min-width: 768px) {
display: inline-block;
width: 33.333333%;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
/*!
* @overview es6-promise - a tiny implementation of Promises/A+.
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
* @license Licensed under MIT license
* See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
* @version v4.2.8+1e68dce6
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="51px" height="44px" viewBox="0 0 51 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Group 7</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="04-docs-start-copy-3" transform="translate(-816.000000, -695.000000)">
<g id="Group-7" transform="translate(816.000000, 695.000000)">
<path d="M25.1572266,19.5799762 L1.12512399,31.5016008 L25.1572266,43.4232253 L49.1893291,31.5016008 L25.1572266,19.5799762 Z" id="Rectangle-3-Copy-3" stroke="#318AD3"></path>
<path d="M25.1572266,10.5986098 L1.12512399,22.5202343 L25.1572266,34.4418588 L49.1893291,22.5202343 L25.1572266,10.5986098 Z" id="Rectangle-3-Copy-5" stroke="#748294"></path>
<path d="M25.1572266,0.558141166 L1.12512399,12.4797657 L25.1572266,24.4013902 L49.1893291,12.4797657 L25.1572266,0.558141166 Z" id="Rectangle-3-Copy-6" stroke="#318AD3"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="37px" height="34px" viewBox="0 0 37 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>terminal</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="04-docs-start-copy-3" transform="translate(-497.000000, -699.000000)">
<g id="terminal" transform="translate(497.000000, 700.000000)">
<polyline id="Shape" stroke="#318AD3" points="0 27 14 13.5 0 0"></polyline>
<path d="M18,32 L36,32" id="Shape" stroke="#748294"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 856 B

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="38px" height="31px" viewBox="0 0 38 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Group 6</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="04-docs-start-copy-3" transform="translate(-170.000000, -702.000000)" fill-rule="nonzero">
<g id="Group-6" transform="translate(170.000000, 702.000000)">
<rect id="Rectangle-2" stroke="#318AD3" x="0.5" y="0.5" width="37" height="30" rx="3"></rect>
<rect id="Rectangle-8" stroke="#318AD3" x="0.5" y="6.5" width="37" height="1"></rect>
<rect id="Rectangle-8" stroke="#748294" x="8.5" y="23.5" width="9" height="1"></rect>
<rect id="Rectangle-8" stroke="#748294" x="8.5" y="25.5" width="9" height="1"></rect>
<rect id="Rectangle-8" stroke="#748294" x="21.5" y="23.5" width="9" height="1"></rect>
<rect id="Rectangle-8" stroke="#748294" x="21.5" y="25.5" width="9" height="1"></rect>
<rect id="Rectangle-9" stroke="#748294" x="5.5" y="11.5" width="26" height="6"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="318px" height="350px" viewBox="0 0 318 350" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Group 2</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="-83.2348064%" y1="106.401351%" x2="140.023112%" y2="-13.2029998%" id="linearGradient-1">
<stop stop-color="#F8FAFC" offset="0%"></stop>
<stop stop-color="#F8FAFC" offset="21.7972142%"></stop>
<stop stop-color="#BBDBF5" offset="100%"></stop>
<stop stop-color="#B9DAF5" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="04-docs-start-copy-3" transform="translate(-714.000000, -205.000000)">
<g id="Group-2" transform="translate(714.000000, 205.000000)">
<path d="M159.9,41.23566 L29.5,106.216315 L29.5,268.671884 L159.9,333.652539 L290.3,268.671884 L290.3,106.216315 L159.9,41.23566 Z" id="Rectangle-3-Copy" stroke="#B9DAF5"></path>
<polygon id="Rectangle-3-Copy-2" fill="url(#linearGradient-1)" points="129.8 186.334516 129.8 349.670807 0 284.336291 0 121"></polygon>
<path d="M317.3,186.64261 L188.5,121.811441 L188.5,284.028197 L317.3,348.859366 L317.3,186.64261 Z" id="Rectangle-3-Copy-4" stroke="#338CD6" transform="translate(252.900000, 235.335404) scale(1, -1) translate(-252.900000, -235.335404) "></path>
<path d="M159.9,41.2359725 L30.1185393,106.090062 L159.9,170.944152 L289.681461,106.090062 L159.9,41.2359725 Z" id="Rectangle-3" stroke="#B9DAF5"></path>
<path d="M159.9,0.558953879 L30.1185393,65.4130435 L159.9,130.267133 L289.681461,65.4130435 L159.9,0.558953879 Z" id="Rectangle-3-Copy-3" stroke="#338CD6"></path>
<path d="M160.175,170.953416 L160.175,332.562112" id="Line" stroke="#B9DAF5" stroke-linecap="square" fill-rule="nonzero"></path>
<path d="M160.358333,130.484509 L160.358333,165.798482" id="Line-2-Copy-3" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero"></path>
<path d="M290.158333,68.178117 L290.158333,103.49209" id="Line-2-Copy-4" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero"></path>
<path d="M133.55,185.742236 L159.95,172.549689" id="Line-2" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero"></path>
<path d="M133.55,348.742236 L159.95,335.549689" id="Line-2-Copy" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero"></path>
<path d="M292.35,283.586957 L318.75,270.39441" id="Line-2" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero" transform="translate(305.550000, 276.990683) scale(1, -1) translate(-305.550000, -276.990683) "></path>
<path d="M162.55,348.742236 L188.95,335.549689" id="Line-2-Copy-2" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero" transform="translate(175.750000, 342.145963) scale(1, -1) translate(-175.750000, -342.145963) "></path>
<path d="M291.55,119.742236 L317.95,106.549689" id="Line-2-Copy" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero" transform="translate(304.750000, 113.145963) scale(1, -1) translate(-304.750000, -113.145963) "></path>
<path d="M162.55,185.742236 L188.95,172.549689" id="Line-2-Copy-5" stroke="#C7CFDA" stroke-linecap="square" stroke-dasharray="2,4" fill-rule="nonzero" transform="translate(175.750000, 179.145963) scale(1, -1) translate(-175.750000, -179.145963) "></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
source/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,8 @@
<svg width="13px" height="13px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-829.000000, -42.000000)" fill="#748294" fill-rule="nonzero">
<path d="M843.319857,54.9056439 L848.707107,60.2928932 C849.097631,60.6834175 849.097631,61.3165825 848.707107,61.7071068 C848.316582,62.0976311 847.683418,62.0976311 847.292893,61.7071068 L841.905644,56.3198574 C840.55096,57.3729184 838.848711,58 837,58 C832.581722,58 829,54.418278 829,50 C829,45.581722 832.581722,42 837,42 C841.418278,42 845,45.581722 845,50 C845,51.8487115 844.372918,53.5509601 843.319857,54.9056439 Z M837,56 C840.313708,56 843,53.3137085 843,50 C843,46.6862915 840.313708,44 837,44 C833.686292,44 831,46.6862915 831,50 C831,53.3137085 833.686292,56 837,56 Z" id="Mask"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 971 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

35
source/branding.blade.md Normal file
View file

@ -0,0 +1,35 @@
---
title: Branding
description: Branding information about Tenancy for Laravel.
extends: _layouts.master
section: content
centered: true
markdown: true
---
# Branding
## Name
The official SEO-friendly name of the project is **Tenancy for Laravel**. When talking about this package in the context of other packages, the repository name **stancl/tenancy** is used too, to clearly distinguish this project from others.
## Color
The main brand color is Tailwind UI's <span class="text-indigo-600">indigo-600</span> &mdash; `#5850EC`.
The color of the "inner box" in the logo is <span class="text-blue-300">blue-300</span> &mdash; `#A4CAFE`.
## Fonts
All texts on this website use the Inter font &mdash; with the exception of the logo which uses Nunito for the upper text.
## Design
You can view our design kit on Figma:
<iframe
height="450"
width="800"
src="https://www.figma.com/embed?embed_host=astra&url=https://www.figma.com/file/JOg9ioEQJJxLlYtpsPkVAb/Tenancy-for-Laravel"
allowfullscreen
></iframe>

65
source/contact.blade.php Normal file
View file

@ -0,0 +1,65 @@
@extends('_layouts.master', ['title' => 'Contact'])
@section('content')
<div class="bg-white py-16 px-4 overflow-hidden sm:px-6 lg:px-8 lg:py-24">
<div class="relative max-w-xl mx-auto">
<div class="text-center">
<h2 class="text-3xl leading-9 font-extrabold tracking-tight text-gray-900 sm:text-4xl sm:leading-10">
Contact me
</h2>
<p class="mt-4 text-lg leading-6 text-gray-500">
You can contact me if you need consulting, want to have your implementation audited, or want to discuss something.
</p>
<p class="mt-4 text-lg leading-6 text-gray-500">
You can also use email: <a class="text-indigo-700" href="mailto:samuel@tenancyforlaravel.com">samuel@tenancyforlaravel.com</a>.
</p>
</div>
<div class="mt-12">
<form netlify class="grid grid-cols-1 row-gap-6 sm:grid-cols-2 sm:col-gap-8">
<div>
<label for="first_name" class="block text-sm font-medium leading-5 text-gray-700">First name
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input id="first_name" class="form-input py-3 px-4 block w-full transition ease-in-out duration-150" />
</div>
</div>
<div>
<label for="last_name" class="block text-sm font-medium leading-5 text-gray-700">Last name
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input id="last_name" class="form-input py-3 px-4 block w-full transition ease-in-out duration-150" />
</div>
</div>
<div class="sm:col-span-2">
<label for="company" class="block text-sm font-medium leading-5 text-gray-700">Company
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input id="company" class="form-input py-3 px-4 block w-full transition ease-in-out duration-150" />
</div>
</div>
<div class="sm:col-span-2">
<label for="email" class="block text-sm font-medium leading-5 text-gray-700">Email
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input id="email" type="email" class="form-input py-3 px-4 block w-full transition ease-in-out duration-150" />
</div>
</div>
<div class="sm:col-span-2">
<label for="message" class="block text-sm font-medium leading-5 text-gray-700">Message
</label>
<div class="mt-1 relative rounded-md shadow-sm">
<textarea id="message" rows="4" class="form-textarea py-3 px-4 block w-full transition ease-in-out duration-150"></textarea>
</div>
</div>
<div class="sm:col-span-2">
<span class="w-full inline-flex rounded-md shadow-sm">
<button type="submit" class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150">
Let's talk
</button>
</span>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url={{ $page->baseUrl . '/docs/' . $page->defaultVersion }}">
<title>stancl/tenancy</title>
</head>
</html>

View file

@ -0,0 +1,51 @@
---
title: Application Testing
description: Application Testing with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Application Testing {#application-testing}
To test your application with this package installed, you can create tenants in the `setUp()` method of your test case:
```php
protected function setUp(): void
{
parent::setUp();
tenant()->create('test.localhost');
tenancy()->init('test.localhost');
}
```
If you're using the database storage driver, you will also need to run the `create_tenants_table` migration:
```php
protected function setUp(): void
{
parent::setUp();
$this->call('migrate', [
'--path' => database_path('migrations'),
'--database' => 'sqlite',
]);
tenant()->create('test.localhost');
tenancy()->init('test.localhost');
}
```
If you're using the Redis storage driver, flush the database in `setUp()`:
```php
protected function setUp(): void
{
parent::setUp();
// make sure you're using a different connection for testing to avoid losing data
Redis::connection('tenancyTesting')->flushdb();
tenant()->create('test.localhost');
tenancy()->init('test.localhost');
}
```

View file

@ -0,0 +1,56 @@
---
title: Configuration
description: Configuring stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Configuration {#configuration}
The `config/tenancy.php` file lets you configure how the package behaves.
> If the `tenancy.php` file doesn't exist in your `config` directory, you can publish it by running `php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config`
### `storage_driver, storage` {#storage}
This lets you configure the driver for tenant storage, i.e. what will be used to store information about your tenants. You can read more about this on the [Storage Drivers]({{ $page->link('storage-drivers') }}) page.
Available storage drivers:
- `Stancl\Tenancy\StorageDrivers\RedisStorageDriver`
- `Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver`
### `tenant_route_namespace` {#tenant-route-namespace}
Controller namespace used for routes in `routes/tenant.php`. The default value is the same as the namespace for `web.php` routes.
### `exempt_domains` {#exempt-domains}
If a hostname from this array is visited, the `tenant.php` routes won't be registered, letting you use the same routes as in that file.
### `database` {#database}
The application's default connection will be switched to a new one — `tenant`. This connection will be based on the connection specified in `tenancy.database.based_on`. The database name will be `tenancy.database.prefix + tenant UUID + tenancy.database.suffix`.
You can set the suffix to `.sqlite` if you're using sqlite and want the files to be with the `.sqlite` extension. Conversely, you can leave the suffix empty if you're using MySQL, for example.
### `redis` {#redis}
If `tenancy.redis.tenancy` is set to true, connections listed in `tenancy.redis.prefixed_connections` will be prefixed with `config('tenancy.redis.prefix_base') . $uuid`.
> Note: You need phpredis for multi-tenant Redis.
### `cache` {#cache}
The `CacheManager` instance that's resolved when you use the `Cache` or the `cache()` helper will be replaced by `Stancl\Tenancy\CacheManager`. This class automatically uses [tags](https://laravel.com/docs/master/cache#cache-tags). The tag will look like `config('tenancy.cache.tag_base') . $uuid`.
If you need to store something in global, non-tenant cache,
### `filesystem` {#filesystem}
The `storage_path()` will be suffixed with a directory named `config('tenancy.filesystem.suffix_base') . $uuid`.
The root of each disk listed in `tenancy.filesystem.disks` will be suffixed with `config('tenancy.filesystem.suffix_base') . $uuid`.
For disks listed in `root_override`, the root will be that string with `%storage_path%` replaced by `storage_path()` *after* tenancy has been initialized. All other disks will be simply suffixed with `tenancy.filesystem.suffix_base` + the tenant UUID.
Read more about this on the [Filesystem Tenancy]({{ $page->link('filesystem-tenancy') }}) page.

View file

@ -0,0 +1,64 @@
---
title: Console Commands
description: Console commands with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Console Commands {#console-commands}
The package comes with some artisan commands that will help you during development.
## Migrate {#migrate}
The most important command. To use tenants, you have to be able to migrate their databases.
You can use the `tenants:migrate` command to migrate tenant's databases. You can also specify which tenants' databases should be migrated using the `--tenants` option.
```
php artisan tenants:migrate --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
> Note: Tenant migrations must be located in `database/migrations/tenant`.
You can use these commands outside the command line as well. If you want to migrate a tenant's database in a controller, you can use the `Artisan` facade.
```php
$tenant = tenant()->create('tenant1.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant['uuid']]
]);
```
## Rollback & seed {#rollback}
- Rollback: `tenants:rollback`
- Seed: `tenants:seed`
Similarly to [migrate](#migrate), these commands accept a `--tenants` option.
## Run {#run}
You can use the tenants:run command to run your own commands for tenants.
If your command's signature were `email:send {--queue} {--subject=} {body}`, you would run this command like this:
```
php artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..."
```
## Tenant list {#tenant-list}
```none
php artisan tenants:list
Listing all tenants.
[Tenant] uuid: dbe0b330-1a6e-11e9-b4c3-354da4b4f339 @ localhost
[Tenant] uuid: 49670df0-1a87-11e9-b7ba-cf5353777957 @ dev.localhost
```
## Selectively clearing tenant cache {#selectively-clearing-tenant-cache}
You can delete specific tenants' cache by using the `--tags` option on `cache:clear`:
```
php artisan cache:clear --tags=tenantdbe0b330-1a6e-11e9-b4c3-354da4b4f339
```
The tag is `config('tenancy.cache.tag_base') . $uuid`.

View file

@ -0,0 +1,30 @@
---
title: Creating Tenants
description: Creating tenants with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Creating Tenants {#creating-tenants}
> **Make sure your database is correctly [configured]({{ $page->link('configuration/#database') }}) before creating tenants.**
To create a tenant, you can use
```php
tenant()->create('tenant1.yourapp.com');
```
> Tip: All domains under `.localhost` are routed to 127.0.0.1 on most operating systems. This is useful for development.
If you want to set some data while creating the tenant, you can pass an array with the data as the second argument:
```php
tenant()->create('tenant2.yourapp.com', [
'plan' => 'free'
]);
```
The `create` method returns an array with tenant information (`uuid`, `domain` and whatever else you supplied).
> Note: Creating a tenant doesn't run [migrations]({{ $page->link('console-commands/#migrate') }}) automatically. You have to do that yourself.

View file

@ -0,0 +1,21 @@
---
title: Custom Database Names
description: Custom Database Names with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Custom Database Names {#custom-database-names}
If you want to specify the tenant's database name, set the `tenancy.database_name_key` configuration key to the name of the key that is used to specify the database name in the tenant storage. You must use a name that you won't use for storing other data, so it's recommended to avoid names like `database` and use names like `_stancl_tenancy_database_name` instead. Then just give the key a value during the tenant creation process:
```php
>>> tenant()->create('example.com', [
'_stancl_tenancy_database_name' => 'example_com'
])
=> [
"uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957",
"domain" => "example.com",
"_stancl_tenancy_database_name" => "example_com",
]
```

View file

@ -0,0 +1,18 @@
---
title: Development
description: Development | stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Development {#development}
## Running tests {#running-tests}
### With Docker {#with-docker}
If you have Docker installed, simply run ./test. When you're done testing, run docker-compose down to shut down the containers.
### Without Docker {#without-docker}
If you run the tests of this package, please make sure you don't store anything in Redis @ 127.0.0.1:6379 db#14. The contents of this database are flushed everytime the tests are run.
Some tests are run only if the CI, TRAVIS and CONTINUOUS_INTEGRATION environment variables are set to true. This is to avoid things like bloating your MySQL instance with test databases.

View file

@ -0,0 +1,22 @@
---
title: Difference Between This Package And Others
description: Difference Between This Package And Others | with stancl/tenancy — A Laravel multi-database tenancy package that respects your code.
extends: _layouts.documentation
section: content
---
# Difference Between This Package And Others
A frequently asked question is the difference between this package and [tenancy/multi-tenant](https://github.com/tenancy/multi-tenant).
Packages like tenancy/multi-tenant and tenancy/tenancy give you an API for making your application multi-tenant. They give you a tenant DB connection, traits to apply on your models, a guide on creating your own tenant-aware cache, etc.
This package makes your application multi-tenant automatically and attempts to make you not have to change (m)any things in your code.
## Which one should you use?
Depends on what you prefer.
If you want full control and make your application multi-tenant yourself, use tenancy/multi-tenant.
If you want to focus on writing your application instead of tenancy implementations, use stancl/tenancy.

View file

@ -0,0 +1,9 @@
---
title: Digging Deeper
description: Digging Deeper | stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Digging Deeper {#digging-deeper}

View file

@ -0,0 +1,47 @@
---
title: The Event System
description: The Event System | stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# The Event System
You can use event hooks to change the behavior of the tenancy boostrapping and tenancy ending processes.
The following events are available:
- `boostrapping`
- `boostrapped`
- `ending`
- `ended`
### Tenant-specific database connection example {#tenant-specific-database-connection-example}
You can hook into these events using `Tenancy::<eventName>`:
```php
\Tenancy::boostrapping(function ($tenantManager) {
if ($tenantManager->tenant['uuid'] === 'someUUID') {
config(['database.connections.someDatabaseConnection' => $tenantManager->tenant['databaseConnection']]);
$tenantManager->database->useConnection('someDatabaseConnection');
return ['database'];
}
});
```
The example above checks whether the current tenant has an uuid of `someUUID`. If yes, it creates a new database connection based on data stored in the tenant's storage. Then it changes the default database connection. Finally, it returns an array of the events that this callback prevents.
The following actions can be prevented:
- database connection switch: `database`
- Redis prefix: `redis`
- CacheManager switch: `cache`
- Filesystem changes: `filesystem`
### Tenant-specific configuration example {#tenant-specific-configuration-example}
Another common use case for events is tenant-specific config:
```php
\Tenancy::bootstrapped(function ($tenantManager) {
config(['some.api.key' => $tenantManager->tenant['api_key']);
});
```

View file

@ -0,0 +1,52 @@
---
title: Filesystem Tenancy
description: Filesystem Tenancy with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Filesystem Tenancy {#filesystem-tenancy}
> Note: It's important to differentiate between storage_path() and the Storage facade. The Storage facade is what you use to put files into storage, i.e. `Storage::disk('local')->put()`. `storage_path()` is used to get the path to the storage directory.
The `storage_path()` will be suffixed with a directory named `config('tenancy.filesystem.suffix_base') . $uuid`.
The root of each disk listed in `tenancy.filesystem.disks` will be suffixed with `config('tenancy.filesystem.suffix_base') . $uuid`.
**However, this alone would cause unwanted behavior.** It would work for S3 and similar disks, but for local disks, this would result in `/path_to_your_application/storage/app/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/`. That's not what we want. We want `/path_to_your_application/storage/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/app/`.
That's what the `root_override` section is for. `%storage_path%` gets replaced by `storage_path()` *after* tenancy has been initialized. The roots of disks listed in the `root_override` section of the config will be replaced accordingly. All other disks will be simply suffixed with `tenancy.filesystem.suffix_base` + the tenant UUID.
Since `storage_path()` will be suffixed, your folder structure will look like this:
![The folder structure](https://i.imgur.com/GAXQOnN.png)
If you write to these directories, you will need to create them after you create the tenant. See the docs for [PHP's mkdir](http://php.net/function.mkdir).
Logs will be saved to `storage/logs` regardless of any changes to `storage_path()`.
One thing that you **will** have to change if you use storage similarly to the example on the image is your use of the helper function `asset()` (that is, if you use it).
You need to make this change to your code:
```diff
- asset("storage/images/products/$product_id.png");
+ tenant_asset("images/products/$product_id.png");
```
Note that all (public) tenant assets have to be in the `app/public/` subdirectory of the tenant's storage directory, as shown in the image above.
This is what the backend of `tenant_asset()` returns:
```php
// TenantAssetsController
return response()->file(storage_path('app/public/' . $path));
```
With default filesystem configuration, these two commands are equivalent:
```php
Storage::disk('public')->put($filename, $data);
Storage::disk('local')->put("public/$filename", $data);
```
If you want to store something globally, simply create a new disk and *don't* add it to the `tenancy.filesystem.disks` config.

View file

@ -0,0 +1,34 @@
---
title: Getting Started
description: Getting started with stancl/tenancy — A Laravel multi-database tenancy package that respects your code.
extends: _layouts.documentation
section: content
---
# Getting Started {#getting-started}
[**stancl/tenancy**](https://github.com/stancl/tenancy) is a Laravel multi-database tenancy package. It is designed in a way that requires you to make no changes to your codebase. Instead of applying traits on models and replacing every single reference to cache by a reference to a tenant-aware cache, the package lets you write your app without thinking about tenancy. It handles tenancy automatically.
> Note: Filesystem is the only thing that can be a little problematic. Be sure to read [that page]({{ $page->link('filesystem-tenancy') }}).
## How does it work? {#how-does-it-work}
A user visits `client1.yourapp.com`. The package identifies the tenant who this domain belongs to, and automatically does the following:
- switches database connection
- replaces the default cache manager
- switches Redis connection
- changes filesystem root paths
The benefits of this being taken care of by the package are:
- separation of concerns: you should write your app, not tenancy implementations
- reliability: you won't have to fear that you forgot to replace a reference to cache by a tenant-aware cache call. This is something you might worry about if you're implementing tenancy into an existing application.
## What is multi-tenancy? {#what-is-multi-tenancy}
Multi-tenancy is the ability to provide your application to multiple customers (who have their own users and other resources) from a single instance of your application. Think Slack, Shopify, etc.
Multi-tenancy can be single-database and multi-database.
**Single-database tenancy** means that your application uses only a single database. The way this is usually implemented is that instead of having the `id`, `title`, `user_id` and `body` columns in your `posts` table, you will also have a `tenant_id` column. This approach works until you need custom databases for your clients. It's also easy to implement, it basically boils down to having your models use a trait which adds a [global scope](https://laravel.com/docs/master/eloquent#global-scopes).
**Multi-database tenancy**, the type that this package provides, lets you use a separate database for each tenant. The benefits of this approach are scalability, compliance (some clients need to have the database on their server) and mitigation of risks such as showing the wrong tenant's data to a user. The downside is that this model is harder to implement, which is why this package exists.

View file

@ -0,0 +1,18 @@
---
title: Horizon Integration
description: Horizon Integration with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Horizon Integration
> Make sure your queue is [correctly configured]({{ $page->link('jobs-queues') }}) before using Horizon.
Jobs are automatically tagged with the tenant's uuid and domain:
![UUID and domain tags](https://i.imgur.com/K2oWTJc.png)
You can use these tags to monitor specific tenants' jobs:
![Monitoring tags](https://i.imgur.com/qB6veK7.png)

View file

@ -0,0 +1,40 @@
---
title: HTTPS Certificates
description: HTTPS Certificates with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# HTTPS certificates
HTTPS certificates are very easy to deal with if you use the `yourclient1.yourapp.com`, `yourclient2.yourapp.com` model. You can use a wildcard HTTPS certificate.
If you use the model where second level domains are used, there are multiple ways you can solve this.
This guide focuses on nginx.
### 1. Use nginx with the lua module
Specifically, you're interested in the [`ssl_certificate_by_lua_block`](https://github.com/openresty/lua-nginx-module#ssl_certificate_by_lua_block) directive. Nginx doesn't support using variables such as the hostname in the `ssl_certificate` directive, which is why the lua module is needed.
This approach lets you use one server block for all tenants.
### 2. Add a simple server block for each tenant
You can store most of your config in a file, such as `/etc/nginx/includes/tenant`, and include this file into tenant server blocks.
```nginx
server {
include includes/tenant;
server_name foo.bar;
# ssl_certificate /etc/foo/...;
}
```
### Generating certificates
You can generate a certificate using certbot. If you use the `--nginx` flag, you will need to run certbot as root. If you use the `--webroot` flag, you only need the user that runs it to have write access to the webroot directory (or perhaps webroot/.well-known is enough) and some certbot files (you can specify these using --work-dir, --config-dir and --logs-dir).
Creating this config dynamically from PHP is not easy, but is probably feasible. Giving `www-data` write access to `/etc/nginx/sites-available/tenants.conf` should work.
However, you still need to reload nginx configuration to apply the changes to configuration. This is problematic and I'm not sure if there is a simple and secure way to do this from PHP.

View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=getting-started">
<title>stancl/tenancy</title>
</head>
</html>

View file

@ -0,0 +1,67 @@
---
title: Installation
description: Installing stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Installation {#getting-started}
Laravel 5.8 is needed.
### Require the package via composer
First you need to require the package using composer:
```
composer require 'stancl/tenancy:~1.8'
```
### Automatic installation {#automatic-installation}
To install the package, simply run
```
php artisan tenancy:install
```
You will be asked if you want to store your data in Redis or a relational database. You can read more about this on the [Storage Drivers]({{ $page->link('storage-drivers') }}) page.
This will do all the steps listed in the [Manual installation](#manual-installation) section for you.
The only thing you have to do now is create a database/Redis connection. Read the [Storage Drivers]({{ $page->link('storage-drivers') }}) page for information about that.
### Manual installation {#manual-installation}
If you prefer installing the package manually, you can do that too. It shouldn't take more than a minute either way.
#### Setting up middleware
Now open `app/Http/Kernel.php` and make the `InitializeTenancy` middleware top priority, so that it gets executed before anything else, making sure things like the database switch connections soon enough:
```php
protected $middlewarePriority = [
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
// ...
];
```
#### Creating routes
The package lets you have tenant routes and "exempt" routes. Tenant routes are your application's routes. Exempt routes are routes exempt from tenancy — landing pages, sign up forms, and routes for managing tenants.
Routes in `routes/web.php` are exempt, whereas routes in `routes/tenant.php` have the `InitializeTenancy` middleware automatically applied on them.
So, to create tenant routes, put those routes in a new file called `routes/tenant.php`.
#### Configuration
Run the following:
```
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config
```
This creates a `config/tenancy.php`. You can use it to configure how the package works.
Configuration is explained in detail on the [Configuration]({{ $page->link('configuration') }}) page.

View file

@ -0,0 +1,12 @@
---
title: Integrations
description: Integrating stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Integrations {#integrations}
This package naturally integrates well with Laravel packages, since it does not rely on you explicitly specifying database connections.
There are some exceptions, though. [Telescope integration]({{ $page->link('telescope') }}), for example, requires you to change the database connection in `config/telescope.php` to a non-default one, because the default connection is switched to the tenant connection. Some packages should use a central connection for data storage.

View file

@ -0,0 +1,23 @@
---
title: Jobs & Queues
description: Jobs & Queues with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Jobs & Queues {#jobs-queues}
Jobs are automatically multi-tenant, which means that if a job is dispatched while tenant A is initialized, the job will operate with tenant A's database, cache, filesystem, and Redis.
**However**, if you're using the `database` or `redis` queue driver, you have to make a small tweak to your queue configuration.
Open `config/queue.php` and make sure your queue driver has an explicitly set connection. Otherwise it would use the default one, which would cause issues, since `database.default` is changed by the package and Redis connections are prefixed.
**If you're using `database`, add a new line to `queue.connections.database`:**
```php
'connection' => 'mysql',
```
where `'mysql'` is the name of your non-tenant database connection with a `jobs` table.
**If you're using Redis, make sure its `'connection'` is not in `tenancy.redis.prefixed_connections`.**

View file

@ -0,0 +1,20 @@
---
title: Middleware Configuration
description: Middleware Configuration with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Middleware Configuration {#middleware-configuration}
When a tenant route is visited and the tenant can't be identified, an exception is thrown. If you want to change this behavior, to a redirect for example, add this to your `app/Providers/AppServiceProvider.php`'s `boot()` method:
```php
// use Stancl\Tenancy\Middleware\InitializeTenancy;
$this->app->bind(InitializeTenancy::class, function ($app) {
return new InitializeTenancy(function ($exception) {
// return redirect()->route('foo');
});
});
```

View file

@ -0,0 +1,36 @@
---
title: Miscellaneous Tips
description: Miscellaneous Tips | stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Miscellaneous Tips {#misc-tips}
## Tenant Redirect {#tenant-redirect}
A customer has signed up on your website, you have created a new tenant and now you want to redirect the customer to their website. You can use the `tenant()` method on Redirect, like this:
```php
// tenant sign up controller
return redirect()->route('dashboard')->tenant($tenant['domain']);
```
## Custom ID scheme
If you don't want to use UUIDs and want to use something more human-readable (even domain concatenated with uuid, for example), you can create a custom class for this:
```php
use Stancl\Tenancy\Interfaces\UniqueIdentifierGenerator;
class MyUniqueIDGenerator implements UniqueIdentifierGenerator
{
public static function handle(string $domain, array $data): string
{
return $domain . \Webpatser\Uuid\Uuid::generate(1, $domain);
}
}
```
and then set the `tenancy.unique_id_generator` config to the full path to your class.

View file

@ -0,0 +1,17 @@
---
title: Stay Updated
description: Stay Updated | stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Stay Updated {#stay-updated}
If you'd like to be notified about new versions, you can [sign up for e-mail notifications](https://stancl.github.io/tenancy/#stay-updated) or join our [Telegram channel](https://t.me/joinchat/AAAAAFjdrbSJg0ZCHTzxLA).
You can choose whether you want to receive emails about major versions and/or minor versions.
- Major versions include breaking changes. Composer won't know about these versions and won't update to them. Major versions will be released about once every 6 months.
- Minor versions include backwards-compatible features and bug fixes.
<!-- todo mailchimp dialog -->

View file

@ -0,0 +1,61 @@
---
title: Storage Drivers
description: Storage Drivers of stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Storage Drivers {#storage-drivers}
Storage drivers are used to store a list of all tenants, their domains and any extra information you store about your tenants (e.g. their plan).
Currently, database and Redis storage drivers are available as part of the package. However, you can [write your own]({{ $page->link('writing-storage-drivers') }}) (and contribute ❤️) storage drivers.
## Database {#database}
The database storage driver lets you store tenant information in a relational database like MySQL, PostgreSQL and SQLite.
The benefit of this storage driver is that you don't have to use both Redis and a database for your data. Also you don't have to do as much configuration.
To use this driver, you need to have a `tenants` table. You may also use a custom database connection. By default, `tenancy.storage.db.connection` is set to `central`, which means that the `central` database connection will be used to store tenants. This connection is not automatically created, so you'd have to create it manually. You can create database connections in the `config/database.php` file.
If you'd like to use an existing connection, you can set this config to the name of the connection, e.g. `mysql`.
To create the `tenants` table, you can use the migration that comes with this package. If you haven't published it during installation, publish it now:
```
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=migrations
```
By default, all of your data will be stored in the JSON column `data`. If you want to store some data in a dedicated column (to leverage indexing, for example), add the column to the migration and to `tenancy.custom_columns` config.
Finally, run the migration:
```
php artisan migrate
```
> If you use a non-default connection, such as `central`, you have to specify which DB to migrate using the `--database` option.
>
> If you have existing migrations related to your app in `database/migrations`, move them to `database/migrations/tenant`. You can read more about tenant migrations [here]({{ $page->link('console-commands/#migrate') }}).
## Redis {#redis}
The Redis storage driver lets you store tenant information in Redis, a high-performance key-value store.
The benefit of this storage driver is its performance.
**Note that you need to configure persistence on your Redis instance if you don't want to lose all information about tenants.**
Read the [Redis documentation page on persistence](https://redis.io/topics/persistence). You should definitely use AOF and if you want to be even more protected from data loss, you can use RDB **in conjunction with AOF**.
If your cache driver is Redis and you don't want to use AOF with it, run two Redis instances. Otherwise, just make sure you use a different database (number) for tenancy and for anything else.
To use this driver, create a new Redis connection in the `database.redis` configuration (`config/database.php`) called `tenancy`.
```php
'tenancy' => [
'host' => env('TENANCY_REDIS_HOST', '127.0.0.1'),
'password' => env('TENANCY_REDIS_PASSWORD', null),
'port' => env('TENANCY_REDIS_PORT', 6380), // different port = separate Redis instance
'database' => env('TENANCY_REDIS_DB', 3), // alternatively, different database number
],
```

View file

@ -0,0 +1,29 @@
---
title: Telescope Integration
description: Telescope Integration with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Telescope Integration
Requests in Telescope are automatically tagged with the tenant uuid and domain:
![Telescope Request with tags](https://i.imgur.com/CEEluYj.png)
This lets you filter requests by uuid and domain:
![Filtering by uuid](https://i.imgur.com/SvbOa7S.png)
![Filtering by domain](https://i.imgur.com/dCJuEr1.png)
If you'd like to set Telescope tags in your own code, e.g. in your `AppServiceProvider`, replace your `Telescope::tag()` call like this:
```php
\Tenancy::integrationEvent('telescope', function ($entry) {
return ['abc']; // your logic
});
```
![Tenancy tags merged with tag abc](https://i.imgur.com/4p1wOiM.png)
Once Telescope 3 is released, you won't have to do this.
To have Telescope working, make sure your `telescope.storage.database.connection` points to a non-tenant connection. It's that way by default, so for most projects, Telescope should work out of the box.

View file

@ -0,0 +1,152 @@
---
title: Tenancy Initialization
description: Tenancy Initialization with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Tenancy Initialization {#tenancy-initialization}
Tenancy can be initialized by calling `tenancy()->init()`. The `InitializeTenancy` middleware calls this method automatically.
You can end a tenancy session using `tenancy()->end()`. This is useful if you need to run multiple tenant sessions or a mixed tenant/non-tenant session in a single request/command.
The `tenancy()->init()` method calls `bootstrap()`.
This method switches database connection, Redis connection (if Redis tenancy is enabled), cache and filesystem root paths.
This page goes through the code that actually makes this happen. You don't have to read this page to use the package, but it will give you insight into the magic that's happening in the background, so that you can be more confident in it.
## Database tenancy {#database-tenancy}
`bootstrap()` runs the following method:
```php
public function switchDatabaseConnection()
{
$this->database->connect($this->getDatabaseName());
}
```
If `tenancy.database_name_key` is set and present in the current tenant's data, the `getDatabaseName()` returns the stored database_name. Otherwise it returns the prefix + uuid + suffix.
```php
public function getDatabaseName($tenant = []): string
{
$tenant = $tenant ?: $this->tenant;
if ($key = $this->app['config']['tenancy.database_name_key']) {
if (isset($tenant[$key])) {
return $tenant[$key];
}
}
return $this->app['config']['tenancy.database.prefix'] . $tenant['uuid'] . $this->app['config']['tenancy.database.suffix'];
}
```
This is passed as an argument to the `connect()` method. This method creates a new database connection and sets it as the default one.
```php
public function connect(string $database)
{
$this->createTenantConnection($database);
$this->useConnection('tenant');
}
public function createTenantConnection(string $database_name)
{
// Create the `tenant` database connection.
$based_on = config('tenancy.database.based_on') ?: config('database.default');
config()->set([
'database.connections.tenant' => config('database.connections.' . $based_on),
]);
// Change DB name
$database_name = $this->getDriver() === 'sqlite' ? database_path($database_name) : $database_name;
config()->set(['database.connections.tenant.database' => $database_name]);
}
public function useConnection(string $connection)
{
// $this->database = Illuminate\Database\DatabaseManager
$this->database->setDefaultConnection($connection);
$this->database->reconnect($connection);
}
```
## Redis tenancy {#redis-tenancy}
The `bootstrap()` method calls `setPhpRedisPrefix()` if `tenancy.redis.tenancy` is `true`.
This method cycles through the `tenancy.redis.prefixed_connections` and sets their prefix to `tenancy.redis.prefix_base` + uuid.
```php
public function setPhpRedisPrefix($connections = ['default'])
{
// [...]
foreach ($connections as $connection) {
$prefix = $this->app['config']['tenancy.redis.prefix_base'] . $this->tenant['uuid'];
$client = Redis::connection($connection)->client();
try {
// [...]
$client->setOption($client::OPT_PREFIX, $prefix);
} catch (\Throwable $t) {
throw new PhpRedisNotInstalledException();
}
}
}
```
## Cache tenancy {#cache-tenancy}
`bootstrap()` calls `tagCache()` which replaces the `'cache'` key in the service container with a different `CacheManager`.
```php
public function tagCache()
{
// [...]
$this->app->extend('cache', function () {
return new \Stancl\Tenancy\CacheManager($this->app);
});
}
```
This `CacheManager` forwards all calls to the inner store, but also adds tag which "scope" the cache and allow for selective cache clearing:
```php
class CacheManager extends BaseCacheManager
{
public function __call($method, $parameters)
{
$tags = [config('tenancy.cache.tag_base') . tenant('uuid')];
if ($method === 'tags') {
if (\count($parameters) !== 1) {
throw new \Exception("Method tags() takes exactly 1 argument. {count($parameters)} passed.");
}
$names = $parameters[0];
$names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/5.7/cache#removing-tagged-cache-items
return $this->store()->tags(\array_merge($tags, $names));
}
return $this->store()->tags($tags)->$method(...$parameters);
}
}
```
## Filesystem tenancy {#filesystem-tenancy}
`bootstrap()` calls `suffiexFilesystemRootPaths()`. This method changes `storage_path()` and the roots of disks listed in `config('tenancy.filesystem.disks)`. You can read more about this on the [Filesystem Tenancy]({{ $page->link('filesystem-tenancy') }}) page.
```php
public function suffixFilesystemRootPaths()
{
// [...]
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . tenant('uuid');
// storage_path()
$this->app->useStoragePath($old['path'] . "/{$suffix}");
// Storage facade
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
// [...]
if ($root = \str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) {
Storage::disk($disk)->getAdapter()->setPathPrefix($root);
} else {
$root = $this->app['config']["filesystems.disks.{$disk}.root"];
Storage::disk($disk)->getAdapter()->setPathPrefix($root . "/{$suffix}");
}
}
// [...]
}
```

View file

@ -0,0 +1,112 @@
---
title: Tenant Manager
description: Tenant Manager | stancl/tenancy — A Laravel multi-database tenancy package that respects your code.
extends: _layouts.documentation
section: content
---
# Tenant Manager {#tenant-manager}
This page documents a couple of other `TenantManager` methods you may find useful.
### Finding tenant using UUID
`find()` is an alias for `getTenantById()`. You may use the second argument to specify the key(s) as a string/array.
```php
>>> tenant()->find('dbe0b330-1a6e-11e9-b4c3-354da4b4f339');
=> [
"uuid" => "dbe0b330-1a6e-11e9-b4c3-354da4b4f339",
"domain" => "localhost",
"foo" => "bar",
]
>>> tenant()->find('dbe0b330-1a6e-11e9-b4c3-354da4b4f339', 'foo');
=> [
"foo" => "bar",
]
>>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339', ['foo', 'domain']);
=> [
"foo" => "bar",
"domain" => "localhost",
]
```
### Getting tenant ID by domain
```php
>>> tenant()->getTenantIdByDomain('localhost');
=> "b3ce3f90-1a88-11e9-a6b0-038c6337ae50"
>>> tenant()->getIdByDomain('localhost');
=> "b3ce3f90-1a88-11e9-a6b0-038c6337ae50"
```
### Finding tenant by domain
You may use the second argument to specify the key(s) as a string/array.
```php
>>> tenant()->findByDomain('localhost');
=> [
"uuid" => "b3ce3f90-1a88-11e9-a6b0-038c6337ae50",
"domain" => "localhost",
]
```
### Accessing the array
You can access the public array tenant of TenantManager like this:
```php
tenancy()->tenant
```
which is an array. If you want to get the value of a specific key from the array, you can use one of the helpers the key on the tenant array as an argument.
```php
tenant('uuid'); // Does the same thing as tenant()->tenant['uuid']
```
### Getting all tenants
This method returns a collection of arrays.
```php
>>> tenant()->all();
=> Illuminate\Support\Collection {#2980
all: [
[
"uuid" => "32e20780-1a88-11e9-a051-4b6489a7edac",
"domain" => "localhost",
],
[
"uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957",
"domain" => "dev.localhost",
],
],
}
>>> tenant()->all()->pluck('domain');
=> Illuminate\Support\Collection {#2983
all: [
"localhost",
"dev.localhost",
],
}
```
### Deleting a tenant
```php
>>> tenant()->delete('dbe0b330-1a6e-11e9-b4c3-354da4b4f339');
=> true
>>> tenant()->delete(tenant()->getTenantIdByDomain('dev.localhost'));
=> true
>>> tenant()->delete(tenant()->findByDomain('localhost')['uuid']);
=> true
```
This doesn't delete the tenant's database. If you want to delete it, save the database name prior to deleting the tenant. You can get the database name using `getDatabaseName()`
```php
>>> tenant()->getDatabaseName(tenant()->findByDomain('laravel.localhost'))
=> "tenant67412a60-1c01-11e9-a9e9-f799baa56fd9"
```

View file

@ -0,0 +1,34 @@
---
title: Tenant Routes
description: Tenant routes with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Tenant Routes {#tenant-routes}
Routes within `routes/tenant.php` will have the `web` middleware group and the `IntializeTenancy` middleware automatically applied on them. This middleware attempts to identify the tenant based on the current hostname. Once the tenant is identified, the database connection, cache, filesystem root paths and, optionally, Redis connection, will be switched.
Just like `routes/web.php`, these routes use the `App\Http\Controllers` namespace.
> If a tenant cannot be identified, anexception will be thrown. If you want to change this behavior (to a redirect, for example) read the [Middleware Configuration]({{ $page->link('middleware-configuration') }}) page.
## Exempt routes {#exempt-routes}
Routes outside the `routes/tenant.php` file will not have the tenancy middleware automatically applied on them. You can apply this middleware manually, though.
If you want some of your, say, API routes to be multi-tenant, simply wrap them in a Route group with this middleware:
```php
use Stancl\Tenancy\Middleware\InitializeTenancy;
Route::middleware(InitializeTenancy::class)->group(function () {
// Route::get('/', 'HelloWorld');
});
```
## Using the same routes for tenant and non-tenant parts of the application {#using-the-same-routes-for-tenant-and-non-tenant-parts-of-the-application}
The `Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains` middleware makes sure 404 is returned when a user attempts to visit a web route on a tenant (non-exempt) domain.
The install command applies this middleware to the `web` group. If you want to do this for another route group, add this middleware manually to that group. You can do this in `app/Http/Kernel.php`.

View file

@ -0,0 +1,48 @@
---
title: Tenant Storage
description: Tenant storage with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Tenant Storage {#tenant-storage}
Tenant storage is where tenants' uuids and domains are stored. You can store things like the tenant's plan, subscription information, and tenant-specific application configuration in tenant storage. You can use these functions:
```php
get (string|array $key, string $uuid = null) // $uuid defaults to the current tenant's UUID
put (string|array $key, mixed $value = null, string $uuid = null) // if $key is array, make sure $value is null
```
To put something into the tenant storage, you can use `put()` or `set()`.
```php
tenancy()->put($key, $value);
tenancy()->set($key, $value); // alias for put()
tenancy()->put($key, $value, $uuid);
tenancy()->put(['key1' => 'value1', 'key2' => 'value2']);
tenancy()->put(['key1' => 'value1', 'key2' => 'value2'], null, $uuid);
```
To get something from the storage, you can use `get()`:
```php
tenancy()->get($key);
tenancy()->get($key, $uuid);
tenancy()->get(['key1', 'key2']);
```
> Note: `tenancy()->get(['key1', 'key2'])` returns an array with values only
Note that $key has to be a string or an array with string keys. The value(s) can be of any data type. Example with arrays:
```php
>>> tenant()->put('foo', ['a' => 'b', 'c' => 'd']);
=> [ // put() returns the supplied value(s)
"a" => "b",
"c" => "d",
]
>>> tenant()->get('foo');
=> [
"a" => "b",
"c" => "d",
]
```

View file

@ -0,0 +1,20 @@
---
title: Usage
description: Usage | stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Usage {#usage}
This chapter describes usage of the package. That includes creating tenants, deleting tenants, storing data in the tenant storage.
Most pages will use the `tenancy()` helper function. This package comes with two helpers - `tenancy()` and `tenant()`. They do the same thing, so you can use the one that reads better given its context.
`tenant()->create()` reads better than `tenancy()->create()`, but `tenancy()->init()` reads better than `tenant()->init()`.
You can pass an argument to the helper function to get a value out of the tenant storage. `tenant('plan')` is identical to [`tenant()->get('plan')`]({{ $page->link('tenant-storage') }}).
The package also comes with two facades. `Tenancy` and `Tenant`. Use what feels the best.
Both the helpers and the facades resolve the `TenantManager` from the service container.

View file

@ -0,0 +1,80 @@
---
title: Writing Storage Drivers
description: Writing Storage Drivers with stancl/tenancy — A Laravel multi-database tenancy package that respects your code..
extends: _layouts.documentation
section: content
---
# Writing Storage Drivers
If you don't want to use the provided DB/Redis storage drivers, you can write your own driver.
To create a driver, create a class that implements the `Stancl\Tenancy\Interfaces\StorageDriver` interface.
For historical reasons, the `TenantManager` will try to json encode/decode data coming from the storage driver. If you want to avoid this, set `public $useJson = false;`. That will make `TenantManager` encode/decode only `put()` and `get()` data, so that data types can be stored correctly.
The DB storage driver has `public $useJson = false;`, while the Redis storage driver doesn't use this property, so it's false by default.
Here's an example:
```php
namespace App\StorageDrivers\MongoDBStorageDriver;
use Stancl\Tenancy\Interfaces\StorageDriver;
class MongoDBStorageDriver implements StorageDriver
{
public $useJson = false;
public function identifyTenant(string $domain): array
{
//
}
public function getAllTenants(array $uuids = []): array
{
//
}
public function getTenantById(string $uuid, array $fields = []): array
{
//
}
public function getTenantIdByDomain(string $domain): ?string
{
//
}
public function createTenant(string $domain, string $uuid): array
{
//
}
public function deleteTenant(string $uuid): bool
{
//
}
public function get(string $uuid, string $key)
{
//
}
public function getMany(string $uuid, array $keys): array
{
//
}
public function put(string $uuid, string $key, $value)
{
//
}
public function putMany(string $uuid, array $values): array
{
//
}
}
```

View file

@ -0,0 +1,158 @@
---
title: Application Testing
description: Application Testing..
extends: _layouts.documentation
section: content
---
# Application Testing {#application-testing}
> Note: At the moment it's not possible to use `:memory:` SQLite databases or the `RefreshDatabase` trait due to the switching of default database. This will hopefully change in the future.
### Initializing tenancy
You can create tenants in the `setUp()` method of your test case:
```php
protected function setUp(): void
{
parent::setUp();
tenancy()->create('test.localhost');
tenancy()->init('test.localhost');
}
```
If you don't want to initialize tenancy before each test, you may want to do something like this:
```php
class TestCase // extends ...
{
protected $tenancy = false;
public function setUp(): void
{
if ($this->tenancy) {
$this->initializeTenancy();
}
}
public function initializeTenancy($domain = 'test.localhost')
{
tenancy()->create($domain);
tenancy()->init($domain);
}
// ...
}
```
And in your individual test classes:
```php
class FooTest
{
protected $tenancy = true;
/** @test */
public function some_test()
{
$this->assertTrue(...);
}
}
```
### Cleanup
To delete tenants & their databases after tests, you may use this:
```php
public function tearDown(): void
{
config([
'tenancy.queue_database_deletion' => false,
'tenancy.delete_database_after_tenant_deletion' => true,
]);
tenancy()->all()->each->delete();
parent::tearDown();
}
```
### Storage setup
If you're using the database storage driver, you will need to run the migrations in `setUp()`:
```php
protected function setUp(): void
{
parent::setUp();
$this->artisan('migrate:fresh');
// ...
}
```
If you're using the Redis storage driver, flush the database in `setUp()`:
```php
protected function setUp(): void
{
parent::setUp();
// make sure you're using a different connection for testing to avoid losing data
Redis::connection('tenancyTesting')->flushdb();
// ...
}
```
### Sample TestCase
Put together, here's a ready-to-use base TestCase for the DB storage driver
```php
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
public function setUp(): void
{
parent::setUp();
$this->artisan('migrate:fresh');
config([
'tenancy.queue_database_creation' => false,
]);
config(['tenancy.exempt_domains' => [
'127.0.0.1',
'localhost',
]]);
}
public function tearDown(): void
{
config([
'tenancy.queue_database_deletion' => false,
'tenancy.delete_database_after_tenant_deletion' => true,
]);
tenancy()->all()->each->delete();
parent::tearDown();
}
}
```
phpunit.xml:
```xml
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value="database/testing.sqlite"/>
```
> Don't forget to create an empty database/testing.sqlite
You may also wish to add `testing.sqlite` to `database/.gitignore`.

View file

@ -0,0 +1,16 @@
---
title: Cached Tenant Lookup
description: Cached Tenant Lookup
extends: _layouts.documentation
section: content
---
# Cached Tenant Lookup {#cached-tenant-lookup}
If you're using the database storage driver, you may want to cache tenant lookup (domain -> tenant id -> `Tenant` object mapping). Running DB queries on each request to identify the tenant is somewhat expensive, as a separate database connection has to be established.
To avoid this, you may want to enable caching.
You may enable this feature by setting the `tenancy.storage_drivers.db.cache_store` config key to the name of your cache store (e.g. `redis`), and optionally setting `cache_ttl` (default is 3600 seconds).
The cache invalidation of course happens automatically, as long as you modify your tenants using `Tenant` objects and not direct DB calls.

View file

@ -0,0 +1,54 @@
---
title: Central App
description: Central App
extends: _layouts.documentation
section: content
---
# Central App {#central-app}
This package uses routes to separate the tenant part of the application from the central part of the application. The central part will commonly include a landing page, sign up form, and some sort of dashboard.
## Central routes {#central-routes}
Routes in the `routes/web.php` file are the central routes. When they are visited, tenancy is *not* intialized and any model, cache call, controller, job dispatch, Redis call and anything else that is called in during this request will be central.
## Central domains {#central-domains}
However, since you don't want routes related to the app on your main domain and sign up forms on tenant domains, you must also define what domains host the central stuff in the `tenancy.exempt_domains` config.
The exempt domains should not include the protocol. For example, you should include `example.com` rather than `https://example.com`.
## Using central things inside the tenant app {#using-central-things-inside-the-tenant-app}
To use central things (databases/caches/Redis connections/filesystems/...) on special places of your tenant app, you may do the following.
### Central database {#central-database}
Create a new connection and use it like `DB::connection($connectionName)->table('foo')->where(...)`
If you want to use models, create a `getConnectionName()` method that returns the name of the central connection
### Central redis {#central-redis}
Create a new connection, *don't* put it into `tenancy.redis.prefixed_connections`, and use it like `Redis::connection('foo')->get('bar')`
### Central cache {#central-cache}
Use the `GlobalCache` facade, or the `global_cache()` helper.
### Central storage {#central-storage}
Create a disk and *don't* add it to `tenancy.filesystem.disks`.
### Central assets {#central-assets}
Mix is intended for template-related assets and as such, it's not scoped to the current tenant.
Alternatively, the package provides a `global_asset()` helper which is a non-tenant-aware replacement for `asset()`, in case you don't want to use `mix()`.
It's recommended to use `mix()` though, due to its features such as version tagging.
### Central queues {#central-queues}
Create a new queue connection with the `central` key set to `true`.

View file

@ -0,0 +1,137 @@
---
title: Configuration
description: Configuring stancl/tenancy
extends: _layouts.documentation
section: content
---
# Configuration {#configuration}
The `config/tenancy.php` file lets you configure how the package behaves.
> If the `tenancy.php` file doesn't exist in your `config` directory, you can publish it by running `php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config`
### `storage_driver, storage` {#storage}
This lets you configure the driver for tenant storage, i.e. what will be used to store information about your tenants. You can read more about this on the [Storage Drivers]({{ $page->link('storage-drivers') }}) page.
Available storage drivers:
- `Stancl\Tenancy\StorageDrivers\RedisStorageDriver`
- `Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver`
#### db {#db-storage-driver}
- `data_column` - the name of column that holds the tenant's data in a single JSON string
- `custom_columns` - list of keys that shouldn't be put into the data column, but into their own column
- `connection` - what database connection should be used to store tenant data (`null` means the default connection)
- `table_names` - the table names used by the models that come with the storage driver
> Note: Don't use the models directly. You're supposed to use [storage]({{ $page->link('tenant-storage') }}) methods on `Tenant` objects.
#### redis {#redis-db-driver}
- `connection` - what Redis connection should be used to store tenant data. See the [Storage Drivers]({{ $page->link('storage-drivers') }}) documentation.
### `tenant_route_namespace` {#tenant-route-namespace}
Controller namespace used for routes in `routes/tenant.php`. The default value is the same as the namespace for `web.php` routes.
### `exempt_domains` {#exempt-domains}
If a hostname from this array is visited, the `tenant.php` routes won't be registered, letting you use the same routes as in that file. This should be the domain without the protocol (i.e., `example.com` rather than `https://example.com`).
### `database` {#database}
The application's default connection will be switched to a new one — `tenant`. This connection will be based on the connection specified in `tenancy.database.based_on`. The database name will be `tenancy.database.prefix + tenant id + tenancy.database.suffix`.
You can set the suffix to `.sqlite` if you're using sqlite and want the files to be with the `.sqlite` extension. Conversely, you can leave the suffix empty if you're using MySQL, for example.
### `redis` {#redis}
If the `RedisTenancyBootstrapper` is enabled (see `bootstrappers` below), any connections listed in `tenancy.redis.prefixed_connections` will be prefixed with `config('tenancy.redis.prefix_base') . $id`.
> Note: You need phpredis. Predis support will dropped by Laravel in version 7.
### `cache` {#cache}
The `CacheManager` instance that's resolved when you use the `Cache` or the `cache()` helper will be replaced by `Stancl\Tenancy\CacheManager`. This class automatically uses [tags](https://laravel.com/docs/master/cache#cache-tags). The tag will look like `config('tenancy.cache.tag_base') . $id`.
### `filesystem` {#filesystem}
The `storage_path()` will be suffixed with a directory named `config('tenancy.filesystem.suffix_base') . $id`.
The root of each disk listed in `tenancy.filesystem.disks` will be suffixed with `config('tenancy.filesystem.suffix_base') . $id`.
For disks listed in `root_override`, the root will be that string with `%storage_path%` replaced by `storage_path()` *after* tenancy has been initialized. All other disks will be simply suffixed with `tenancy.filesystem.suffix_base` + the tenant id.
Read more about this on the [Filesystem Tenancy]({{ $page->link('filesystem-tenancy') }}) page.
### `database_managers` {#database_managers}
Tenant database managers handle the creation & deletion of tenant databases. This configuration array maps the database driver name to the `TenantDatabaseManager`, e.g.:
```php
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class
```
### `database_manager_connections` {#database_maanger_connections}
Connections used by TenantDatabaseManagers. They tell, for example, that the manager for the `mysql` driver (`MySQLDatabaseManager`) should use the `mysql` connection. You may want to change this if your connection is named differently, e.g. a MySQL connection named `central`.
### `bootstrappers` {#bootstrappers}
These are the classes that do the magic. When tenancy is initialized, TenancyBootstrappers are executed, making Laravel tenant-aware.
This config is an array. The key is the alias and the value is the full class name.
```php
'cache' => Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper::class,
```
The aliases are used by the [event system]({{ $page->link('hooks') }})
### `features` {#bootstrappers}
[Features]({{ $page->link('optional-features') }}) are similar to bootstrappers, but they are executed regardless of whether tenancy has been initialized or not. Their purpose is to provide additional functionality beyond what is necessary for the package to work. Things like easy redirects to tenant domains, tags in Telescope, etc.
### `home_url` {#home-url}
When a user tries to visit a non-tenant route on a tenant domain, the `PreventAccessFromTenantDomains` middleware will return a redirect to this url.
### `queue_database_creation` {#queue-database-creation}
- Default: `false`
### `migrate_after_creation` {#migrate-after-creation}
Run migrations after creating a tenant.
- Default: `false`
### `seed_after_migration` {#seed-after-migration}
Run seeds after creating a tenant.
- Default: `false`
### `seeder_parameters` {#seeder_parameters}
Parameters passed to the `tenants:seed` command.
- Default: `['--class' => 'DatabaseSeeder']`
### `delete_database_after_tenant_deletion` {#delete-database-after-tenant-deletion}
Delete the tenant's database after deleting the tenant.
- Default: `false`
### `queue_database_deletion` {#queue-database-deletion}
- Default: `false`
### `unique_id_generator` {#unique-id-generator}
The class used to generate a random tenant ID (when no ID is supplied during the tenant creation process).
- Default: `Stancl\Tenancy\UUIDGenerator`

View file

@ -0,0 +1,91 @@
---
title: Console Commands
description: Console commands..
extends: _layouts.documentation
section: content
---
# Console Commands {#console-commands}
The package comes with some artisan commands that will help you during development.
## Migrate {#migrate}
The most important command. To use tenants, you have to be able to migrate their databases.
You can use the `tenants:migrate` command to migrate tenant's databases. You can also specify which tenants' databases should be migrated using the `--tenants` option.
```
php artisan tenants:migrate --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
You may use multiple `--tenants=<...>` options.
> Note: Tenant migrations must be located in `database/migrations/tenant`.
You can use these commands outside the command line as well. If you want to migrate a tenant's database in a controller, you can use the `Artisan` facade.
```php
$tenant = tenancy()->create('tenant1.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant['id']]
]);
```
## Rollback & seed {#rollback}
- Rollback: `tenants:rollback`
- Seed: `tenants:seed`
Similarly to [migrate](#migrate), these commands accept a `--tenants` option.
## Migrate fresh {#migrate-fresh}
This package also offers a simplified, tenant-aware version of `migrate:fresh`. It runs `db:wipe` and `tenants:migrate` on the tenant's database.
You may use it like this:
```none
php artisan tenants:migrate-fresh --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23
```
## Run {#run}
You can use the `tenants:run` command to run your own commands for tenants.
If your command's signature were `email:send {--queue} {--subject=} {body}`, you would run this command like this:
```
php artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..."
```
## Tenant list {#tenant-list}
```none
php artisan tenants:list
Listing all tenants.
[Tenant] id: dbe0b330-1a6e-11e9-b4c3-354da4b4f339 @ localhost
[Tenant] id: 49670df0-1a87-11e9-b7ba-cf5353777957 @ dev.localhost
```
## Create tenant {#create-tenant}
This command lets you create tenants from the command line. You may find this useful if you need to create tenants from some service that's separate from your app.
You may specify any amount of domains using `-d <domain>`. To set data during the creation process, add arguments of the `<key>=<value>` format to the end of the call.
For example:
```none
php artisan tenants:create -d aaa.localhost -d bbb.localhost plan=free email=foo@test.local
5f6dbfb8-41da-4398-a361-5342a98d81a0
```
The command returns the created tenant's id.
## Selectively clearing tenant cache {#selectively-clearing-tenant-cache}
You can delete specific tenants' cache by using the `--tags` option on `cache:clear`:
```
php artisan cache:clear --tags=tenantdbe0b330-1a6e-11e9-b4c3-354da4b4f339
```
The tag is derived from `config('tenancy.cache.tag_base') . $id`.

View file

@ -0,0 +1,38 @@
---
title: Creating Tenants
description: Creating tenants
extends: _layouts.documentation
section: content
---
# Creating Tenants {#creating-tenants}
> **Make sure your database is correctly [configured]({{ $page->link('configuration/#database') }}) before creating tenants.**
To create a tenant, you can use
```php
use Stancl\Tenancy\Tenant;
Tenant::new()
->withDomains(['tenant1.yourapp.com', 'tenant1.com'])
->withData(['plan' => 'free'])
->save();
```
> Tip: All domains under `.localhost` are routed to 127.0.0.1 on most operating systems. This is useful for development.
The `withDomains()` and `withData()` methods are optional.
You can also create a tenant using a single method: `Tenant::create`:
```php
$domains = ['tenant1.myapp.com', 'tenant1.com'];
Tenant::create($domains, [
'plan' => 'free',
]);
```
`Tenant::create()` works with both `Stancl\Tenancy\Tenant` and the facade, `\Tenant`.
> Note: By default, creating a tenant doesn't run [migrations]({{ $page->link('tenant-migrations' )}}) automatically. You may change this behavior using the `migrate_after_creation` [configuration]({{ $page->link('configuration#migrate-after-creation') }}).

View file

@ -0,0 +1,20 @@
---
title: Custom Database Names
description: Custom Database Names..
extends: _layouts.documentation
section: content
---
# Custom Database Names {#custom-database-names}
To set a specific database name for a tenant, set the `_tenancy_db_name` key in the tenant's storage.
You should do this during the tenant creation process, to make sure the right database name is used during database creation.
```php
use Stancl\Tenancy\Tenant;
Tenant::create('example.com', [
'_tenancy_db_name' => 'example_com'
])
```

View file

@ -0,0 +1,26 @@
---
title: Custom Database Connections
description: Custom Database Connections
extends: _layouts.documentation
section: content
---
# Custom Database Connections {#custom-database-names}
To set a specific database connection for a tenant, set the `_tenancy_db_connection` key in the tenant's storage. The connection's database name will be still replaced by the tenant's database name. You can [customize that]({{ $page->link('custom-database-names') }}) too.
You may want custom connections to be dynamic (rather than adding them to the DB config manually), so can use something like this:
```php
// Make new tenants use your connection "template"
Tenant::new()->withData([
'_tenancy_db_connection' => 'someTenantConnectionTemplate',
]);
// Make tweaks to the connection before bootstrapping tenancy
tenancy()->hook('bootstrapping', function ($tenantManager) {
config(['database.connections.someTenantConnectionTemplate.name' => $tenantManager->getTenant('database_name')]);
config(['database.connections.someTenantConnectionTemplate.password' => $tenantManager->getTenant('database_password')]);
config(['database.connections.someTenantConnectionTemplate.host' => $tenantManager->getTenant('database_host')]);
});
```

View file

@ -0,0 +1,41 @@
---
title: Difference Between This Package And Others
description: Difference Between This Package And Others
extends: _layouts.documentation
section: content
---
# Difference Between This Package And Others
A frequently asked question is the difference between this package and other tenancy packages for Laravel.
## tenancy/multi-tenant (previously hyn/multi-tenant)
tenancy/multi-tenant gives you an API for making your application multi-tenant yourself. It gives you a tenant DB connection, traits to apply on your models, a guide on creating your own tenant-aware cache, etc.
## tenancy/tenancy (currently in alpha)
tenancy/tenancy is even less opinionated and is more of a framework to write your own tenancy implementation. For example, there is no tenant object. There is a tenant interface that you implement on some model in your application.
## stancl/tenancy
This package takes a completely new approach to multi-tenancy.
It makes your application multi-tenant automatically and attempts to make you not have to change anything in your code. The philosophy behind this approach is that you should write your app, not tenancy boilerplate.
We belive that your code will be a lot cleaner if tenancy and the actual app don't mix. Why pollute your code with tons of tenancy implementations, when you can push all of tenancy one layer below your actual application?
Apart from saving you a ton of time, the benefit of going with the automatic approach is that you can adapt easily, since you're not bound to a specific implementation of multi-tenancy. [You can always change how tenancy is bootstrapped.]({{ $page->link('tenancy-bootstrappers') }})
This approach is also more secure. Say you have written a SaaS. The application is finished &mdash; now you just need to make it multi-tenant before releasing it. With the tenancy/\* packages, you will have to rewrite significant portions of your code and hope you did not forget to change each, for example, `Cache::` call to some tenant-aware cache that you implement yourself.
With stancl/tenancy, you just install the package, decide what routes belong to the "central" part of the app and what routes belong to the tenant part of the app, and tell the config file on what domains you host the central app &mdash; the landing page & sign up form.
Everything else happens automatically in the background:
- Database connection is switched
- Cache is made multi-tenant
- Filesystem is made multi-tenant
- Queued jobs are made multi-tenant
- Redis is made multi-tenant
This means that you can also integrate with any packages you would normally use, without any difficulties.

View file

@ -0,0 +1,9 @@
---
title: Digging Deeper
description: Digging Deeper..
extends: _layouts.documentation
section: content
---
# Digging Deeper {#digging-deeper}

View file

@ -0,0 +1,16 @@
---
title: Disabling Database Creation
description: Disabling Database Creation
extends: _layouts.documentation
section: content
---
# Disabling Database Creation {#disabling-database-creation}
DB creation can be disabled for all tenants (`tenancy.create_database` config), or for individual tenants during tenant creation:
```php
Tenant::new()->withData([
'_tenancy_create_database' => false,
])->save();
```

View file

@ -0,0 +1,12 @@
---
title: Optional Features
description: Optional Features
extends: _layouts.documentation
section: content
---
# Optional Features {#optional-features}
Similarly to [TenancyBootstrappers]({{ $page->link('tenancy-bootstrappers') }}), `Features` are classes that provide extra functionality and can be enabled in the config. They're all disabled by default, because they're not critical to the core tenancy scaffolding.
You may enable and disable `Features` in the `tenancy.features` configuration.

View file

@ -0,0 +1,34 @@
---
title: Tenant Config
description: Tenant Config
extends: _layouts.documentation
section: content
---
# Tenant Config {#tenant-config}
It's likely you will need to use tenant-specific config in your application. That config could be API keys, things like "products per page" and many other things.
You could just use the [tenant storage]({{ $page->link('tenant-storage') }}) to get these values, but you may still want to use Laravel's `config()` because of:
- separation of concerns -- if you just write tenancy implementation-agnostic `config('shop.products_per_page')`, you will have a much better time changing tenancy implementations
- default values -- you may want to use the tenant storage only to override values in your config file
## Enabling the feature
Uncomment the following line in your `tenancy.features` config:
```php
// Stancl\Tenancy\Features\TenantConfig::class,
```
## Configuring the mappings
This feature maps keys in the tenant storage to config keys based on the `tenancy.storage_to_config_map` config.
For example, if your `storage_to_config_map` looked like this:
```php
'storage_to_config_map' => [
'paypal_api_key' => 'services.paypal.api_key',
],
```
the value of `paypal_api_key` in [tenant storage]({{ $page->link('tenant-storage') }}) would be copied to the `services.paypal.api_key` config when tenancy is initialized.

View file

@ -0,0 +1,17 @@
---
title: Tenant Redirect
description: Tenant Redirect
extends: _layouts.documentation
section: content
---
# Tenant Redirect {#tenant-redirect}
> To enable this feature, uncomment the `Stancl\Tenancy\Features\TenantRedirect::class` line in your `tenancy.features` config.
A customer has signed up on your website, you have created a new tenant and now you want to redirect the customer to their website. You can use the `tenant()` method on Redirect, like this:
```php
// tenant sign up controller
return redirect()->route('dashboard')->tenant($domain);
```

View file

@ -0,0 +1,16 @@
---
title: Timestamps
description: Timestamps
extends: _layouts.documentation
section: content
---
# Timestamps {#timestamps-redirect}
> To enable this feature, uncomment the `Stancl\Tenancy\Features\Timestamps::class` line in your `tenancy.features` config.
This `Feature` adds the following timestamps into the tenant storage:
- `created_at`
- `updated_at`
- `deleted_at` - for soft deletes

View file

@ -0,0 +1,69 @@
---
title: Filesystem Tenancy
description: Filesystem Tenancy..
extends: _layouts.documentation
section: content
---
# Filesystem Tenancy {#filesystem-tenancy}
> Note: It's important to differentiate between storage_path() and the Storage facade. The Storage facade is what you use to put files into storage, i.e. `Storage::disk('local')->put()`. `storage_path()` is used to get the path to the storage directory.
The `storage_path()` will be suffixed with a directory named `config('tenancy.filesystem.suffix_base') . $id`.
The root of each disk listed in `tenancy.filesystem.disks` will be suffixed with `config('tenancy.filesystem.suffix_base') . $id`.
**However, this alone would cause unwanted behavior.** It would work for S3 and similar disks, but for local disks Laravel does its own suffixing. For local storage we need the second of these examples:
```
/path_to_your_application/storage/app/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/
/path_to_your_application/storage/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/app/
```
Why? Because `storage_path()` returns:
`/path_to_your_application/storage/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/`
so Laravel's `storage_path('app')` means appending `app` to that.
That's what the `root_override` section is for. `%storage_path%` gets replaced by `storage_path()` *after* tenancy has been initialized. The roots of disks listed in the `root_override` section of the config will be replaced accordingly. All other disks will be simply suffixed with `tenancy.filesystem.suffix_base` + the tenant id.
Since `storage_path()` will be suffixed, your folder structure will look like this:
![The folder structure](https://i.imgur.com/GAXQOnN.png)
If you write to these directories, you will need to create them after you create the tenant. See the docs for [PHP's mkdir](http://php.net/function.mkdir).
Logs will be saved to `storage/logs` regardless of any changes to `storage_path()`, and regardless of tenant.
## Assets {#assets}
Laravel's `asset()` helper has two different paths of execution:
- If `config('app.asset_url')` has been set, it will simply append `tenant$id` to the end of the configured asset URL. This is useful if you use Laravel Vapor. Vapor sets the asset URL to something like `https://abcdefghijkl.cloudfrount.net/123-456-789`. That is the root for your assets. This package will append that with something like `tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd`.
- If `config('app.asset_url')` is null, as it is by default, the helper will return a URL (`/tenancy/assets/...`) to a controller provided by this package. That controller returns a file response from `storage_path("app/public/$path")`. This means that you need to store your assets in the public directory.
> Note: In 1.x, the `asset()` helper was not tenant-aware, and there was a `tenant_asset()` helper that followed the second option in the list above (a link to a controller). For backwards compatibility, that helper remains intact.
> If you have some non-tenant-specific assets, you may use the package's `global_asset()` helper.
Note that all tenant assets have to be in the `app/public/` subdirectory of the tenant's storage directory, as shown in the image above.
This is what the backend of `tenant_asset()` (and `asset()` when no asset URL is configured) returns:
```php
// TenantAssetsController
return response()->file(storage_path('app/public/' . $path));
```
With default filesystem configuration, these two commands are equivalent:
```php
Storage::disk('public')->put($filename, $data);
Storage::disk('local')->put("public/$filename", $data);
```
Note that every request for a tenant asset requires a full framework boot and tenancy initialization. This is not ideal if you have some assets that occur on each page (like logos). So for non-private assets, you may want to create a disk and use URLs from that disk instead. For example:
```php
Storage::disk('app-public')->url('tenants/logos/' . tenant()->id . '.png');
```
If you want to store something globally, simply create a new disk and *don't* add it to the `tenancy.filesystem.disks` config.

View file

@ -0,0 +1,34 @@
---
title: Getting Started
description: Getting started.
extends: _layouts.documentation
section: content
---
# Getting Started {#getting-started}
[**stancl/tenancy**](https://github.com/stancl/tenancy) is a Laravel multi-database tenancy package. It makes your app multi-tenant in a way that requires no changes to the codebase. Instead of applying traits on models and replacing every single reference to cache by a reference to a tenant-aware cache, the package lets you write your app without thinking about tenancy. It handles tenancy automatically in the background.
## How does it work? {#how-does-it-work}
A user visits `client1.yourapp.com`. The package identifies the tenant who this domain belongs to, and automatically does the following:
- switches database connection
- replaces the default cache manager
- switches Redis connection
- changes filesystem root paths
- makes jobs automatically tenant-aware
The benefits of this being taken care of by the package are:
- separation of concerns: you should write your app, not tenancy implementations
- reliability: you won't have to fear that you forgot to replace a reference to cache by a tenant-aware cache call. This is something you might worry about if you're implementing tenancy into an existing application.
## What is multi-tenancy? {#what-is-multi-tenancy}
Multi-tenancy is the ability to provide your application to multiple customers (who have their own users and other resources) from a single instance of your application. Think Slack, Shopify, etc.
Multi-tenancy can be single-database and multi-database.
**Single-database tenancy** means that your application uses only a single database. The way this is usually implemented is that instead of having the `id`, `title`, `user_id` and `body` columns in your `posts` table, you will also have a `tenant_id` column. This approach works until you need custom databases for your clients. It's also easy to implement, it basically boils down to having your models use a trait which adds a [global scope](https://laravel.com/docs/master/eloquent#global-scopes).
Again, to be clear: This package does not provide single-database tenancy features.
**Multi-database tenancy**, the type that this package provides, lets you use a separate database for each tenant. The benefits of this approach are scalability, compliance (some clients need to have the database on their server) and mitigation of risks such as showing the wrong tenant's data to a user.

View file

@ -0,0 +1,86 @@
---
title: Hooks / The Event System
description: Hooks / The Event System
extends: _layouts.documentation
section: content
---
# Hooks / The Event System
You can use event hooks to change the behavior of the package.
All hook callbacks receive the `TenantManager` as the first argument.
## Tenant events
A common use case for these events is seeding the tenant data during creation:
```php
// AppServiceProvider::boot()
tenancy()->hook('tenant.creating', function (TenantManager $tm, Tenant $tenant) {
$tenant->put([
'posts_per_page' => '15',
]);
});
```
The following events are available:
- `tenant.creating`
- `tenant.created`
- `tenant.updating`
- `tenant.updated`
- `tenant.deleting`
- `tenant.deleted`
- `tenant.softDeleting`
- `tenant.softDeleted`
Callbacks for these events may accept the following arguments:
```php
TenantManager $tenantManager, Tenant $tenant
```
## Database events
A use case for these events is executing something after the tenant database is created (& migrated/seeded) without running into race conditions.
Say you have a `AfterCreatingTenant` job that creates a superadmin user. You may use the `database.creating` event to add this job into the queue chain of the job that creates the tenant's database.
```php
tenancy()->hook('database.creating', function (TenantManager $tm, string $db, Tenant $tenant) {
return [
new AfterCreatingTenant($tenant->id);
]
});
```
The following events are available:
- `database.creating`
- `database.created`
- `database.deleting`
- `database.deleted`
Callbacks for these events may accept the following arguments:
```php
TenantManager $tenantManager, string $db, Tenant $tenant
```
## Bootstrapping/ending events
The following events are available:
- `bootstrapping`
- `bootstrapped`
- `ending`
- `ended`
You may use the `bootstrapping` & `ending` events to prevent some bootstrappers from being executed.
The following actions can be prevented:
- database connection switch: `database`
- Redis prefix: `redis`
- CacheManager switch: `cache`
- Filesystem changes: `filesystem`
- Queue tenancy: `queue`
- and anything else listed in the [`tenancy.bootstrappers` config]({{ $page->link('configuration#bootstrappers') }})
Callbacks for these events may accept the following arguments:
```php
TenantManager $tenantManager, Tenant $tenant
```

View file

@ -0,0 +1,18 @@
---
title: Horizon Integration
description: Horizon Integration..
extends: _layouts.documentation
section: content
---
# Horizon Integration
> Make sure your queue is [correctly configured]({{ $page->link('jobs-queues') }}) before using Horizon.
Jobs are automatically tagged with the tenant's id and domain:
![id and domain tags](https://i.imgur.com/K2oWTJc.png)
You can use these tags to monitor specific tenants' jobs:
![Monitoring tags](https://i.imgur.com/qB6veK7.png)

View file

@ -0,0 +1,40 @@
---
title: HTTPS Certificates
description: HTTPS Certificates..
extends: _layouts.documentation
section: content
---
# HTTPS Certificates
HTTPS certificates are very easy to deal with if you use the `yourclient1.yourapp.com`, `yourclient2.yourapp.com` model. You can use a wildcard HTTPS certificate.
If you use the model where second level domains are used, there are multiple ways you can solve this.
This guide focuses on nginx.
### 1. Use nginx with the lua module
Specifically, you're interested in the [`ssl_certificate_by_lua_block`](https://github.com/openresty/lua-nginx-module#ssl_certificate_by_lua_block) directive. Nginx doesn't support using variables such as the hostname in the `ssl_certificate` directive, which is why the lua module is needed.
This approach lets you use one server block for all tenants.
### 2. Add a simple server block for each tenant
You can store most of your config in a file, such as `/etc/nginx/includes/tenant`, and include this file into tenant server blocks.
```nginx
server {
include includes/tenant;
server_name foo.bar;
# ssl_certificate /etc/foo/...;
}
```
### Generating certificates
You can generate a certificate using certbot. If you use the `--nginx` flag, you will need to run certbot as root. If you use the `--webroot` flag, you only need the user that runs it to have write access to the webroot directory (or perhaps webroot/.well-known is enough) and some certbot files (you can specify these using --work-dir, --config-dir and --logs-dir).
Creating this config dynamically from PHP is not easy, but is probably feasible. Giving `www-data` write access to `/etc/nginx/sites-available/tenants.conf` should work.
However, you still need to reload nginx configuration to apply the changes to configuration. This is problematic and I'm not sure if there is a simple and secure way to do this from PHP.

View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=getting-started">
<title>stancl/tenancy</title>
</head>
</html>

View file

@ -0,0 +1,77 @@
---
title: Installation
description: Installing stancl/tenancy
extends: _layouts.documentation
section: content
---
# Installation {#getting-started}
Laravel 6.0 or higher is needed.
### Require the package via composer
First you need to require the package using composer:
```
composer require stancl/tenancy
```
### Automatic installation {#automatic-installation}
To install the package, simply run
```
php artisan tenancy:install
```
This will do all the steps listed in the [Manual installation](#manual-installation) section for you.
You will be asked if you want to store your data in a relational database or Redis. Continue to the next page ([Storage Drivers]({{ $page->link('storage-drivers') }})) to know what that means.
### Manual installation {#manual-installation}
If you prefer installing the package manually, you can do that too. It shouldn't take more than a minute either way.
#### Setting up middleware
Now open `app/Http/Kernel.php` and make the package's middleware classes top priority, so that they get executed before anything else, making sure things like the database switch connections soon enough:
```php
protected $middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
// ...
];
```
Add the `\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class` middleware to all route groups you use, so that's probably `'web'` and possibly `'api'`:
```php
protected $middlewareGroups = [
'web' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
// ...
],
// ...
]
```
#### Creating routes
The package lets you have tenant routes and "exempt" routes. Tenant routes are your application's routes. Exempt routes are routes exempt from tenancy — landing pages, sign up forms, and routes for managing tenants.
Routes in `routes/web.php` are exempt, while routes in `routes/tenant.php` have the tenancy middleware automatically applied to them.
So, to create tenant routes, put those routes in a new file called `routes/tenant.php`.
#### Configuration
Run the following:
```
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config
```
This creates a `config/tenancy.php`. You can use it to configure how the package works.
Configuration is explained in detail on the [Configuration]({{ $page->link('configuration') }}) page.

View file

@ -0,0 +1,14 @@
---
title: Integrations
description: Integrating stancl/tenancy
extends: _layouts.documentation
section: content
---
# Integrations {#integrations}
This package naturally integrates well with Laravel packages, since it does not rely on you explicitly specifying database connections.
There are some exceptions, though. [Telescope integration]({{ $page->link('telescope') }}), for example, requires you to change the database connection in `config/telescope.php` to a non-default one, because the default connection is switched to the tenant connection. Some packages should use a central connection for data storage.
> You may be thinking, why does the DB storage driver work with the default, central DB connection, but Telescope doesn't? It's because the DB storage driver intelligently uses the **original** default DB connection, if it has been changed.

View file

@ -0,0 +1,25 @@
---
title: Jobs & Queues
description: Jobs & Queues..
extends: _layouts.documentation
section: content
---
# Jobs & Queues {#jobs-queues}
Jobs are automatically multi-tenant, which means that if a job is dispatched while tenant A is initialized, the job will operate with tenant A's database, cache, filesystem, and Redis.
**However**, if you're using the `database` or `redis` queue driver, you have to make a small tweak to your queue configuration.
Open `config/queue.php` and make sure your queue driver has an explicitly set connection. Otherwise it would use the default one, which would cause issues, since `database.default` is changed by the package and Redis connections are prefixed.
**If you're using `database`, add a new line to `queue.connections.database` and `queue.failed`:**
```php
'connection' => 'mysql',
```
where `'mysql'` is the name of your non-tenant database connection with a `jobs` table.
Also make sure you run the queue migrations **for the central database**, not your tenants.
**If you're using Redis, make sure its `'connection'` is not in `tenancy.redis.prefixed_connections`.**

View file

@ -0,0 +1,21 @@
---
title: Livewire Integration
description: Livewire Integration
extends: _layouts.documentation
section: content
---
# Livewire Integration {#livewire-integration}
Open the `config/livewire.php` file and change this line:
```php
'middleware_group' => ['web'],
```
to this:
```php
'middleware_group' => ['web', 'universal'],
```
Now you can use Livewire both in the central app and the tenant app.

View file

@ -0,0 +1,46 @@
---
title: Middleware Configuration
description: Middleware Configuration..
extends: _layouts.documentation
section: content
---
# Middleware Configuration {#middleware-configuration}
## Header or query parameter based identification {#header-or-query-parameter-based-identification}
To identify tenants using request headers or query parameters, you may use the `InitializeTenancyByRequestData` middleware.
Create a **central** route (don't apply the `tenancy` middleware group on it and don't put it into `routes/tenant.php`) and apply this middleware on the route:
```php
\Stancl\Tenancy\Middleware\InitializeTenancyByRequestData::class
```
To customize the header, query parameter, and `onFail` logic, you may do this in your `AppServiceProvider::boot()`:
```php
// use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData::class;
$this->app->bind(InitializeTenancyByRequestData::class, function ($app) {
return new InitializeTenancyByRequestData('header name', 'query parameter', function ($exception) {
// return redirect()->route('foo');
});
});
```
To disable identification using header or query parameter, set the respective parameter to `null`.
## Customizing the onFail logic {#customizing-the-onfail-logic}
When a tenant route is visited and the tenant can't be identified, an exception is thrown. If you want to change this behavior, to a redirect for example, add this to your `app/Providers/AppServiceProvider.php`'s `boot()` method:
```php
// use Stancl\Tenancy\Middleware\InitializeTenancy;
$this->app->bind(InitializeTenancy::class, function ($app) {
return new InitializeTenancy(function ($exception, $request, $next) {
// return redirect()->route('foo');
});
});
```

View file

@ -0,0 +1,28 @@
---
title: Miscellaneous Tips
description: Miscellaneous Tips..
extends: _layouts.documentation
section: content
---
# Miscellaneous Tips {#misc-tips}
## Custom ID scheme
If you don't want to use UUIDs and want to use something more human-readable (even domain concatenated with uuid, for example), you can create a custom class for this:
```php
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
class MyUniqueIDGenerator implements UniqueIdentifierGenerator
{
public static function generate(array $domains, array $data = []): string
{
return $domains[0] . \Ramsey\Uuid\Uuid::uuid4()->toString();
}
}
```
and then set the `tenancy.unique_id_generator` config to the full path to your class.
Note that you may have to make the `id` column on the `tenants` table larger, as it's set to the exact length of uuids by default.

View file

@ -0,0 +1,36 @@
---
title: Nova Integration
description: Nova Integration
extends: _layouts.documentation
section: content
---
# Nova Integration {#nova-integration}
To use Nova inside of the tenant part of your application, do the following:
- Publish the Nova migrations and move them to the `database/migrations/tenant` directory.
```none
php artisan vendor:publish --tag=nova-migrations
```
- Prevent Nova from adding its migrations to your central migrations by adding `Nova::ignoreMigrations()` to `NovaServiceProvider::boot()` (Don't do this if you want to use Nova [both in the central & tenant parts]({{ $page->link('universal-routes') }}) of the app.)
- Add the `'tenancy'` middleware group to your `nova.middleware` config. Example:
```php
'middleware' => [
'tenancy',
'web',
Authenticate::class,
DispatchServingNovaEvent::class,
BootTools::class,
Authorize::class,
],
```
- In your `NovaServiceProvider`'s `routes()` method, replace the following lines:
```php
->withAuthenticationRoutes()
->withPasswordResetRoutes()
```
with these lines:
```php
->withAuthenticationRoutes(['web', 'tenancy'])
->withPasswordResetRoutes(['web', 'tenancy'])
```

View file

@ -0,0 +1,41 @@
---
title: Passport Integration
description: Passport Integration
extends: _layouts.documentation
section: content
---
# Passport Integration {#passport-integration}
> If you just want to write an SPA, but don't need an API for some other use (e.g. mobile app), you can avoid **a lot** of the complexity of writing SPAs by using [Inertia.js](https://inertiajs.com).
To use Passport inside the tenant part of your application, you may do the following.
- Add this to the `register` method in your `AppServiceProvider`:
```php
Passport::ignoreMigrations();
Passport::routes(null, ['middleware' => 'tenancy']);
```
- `php artisan vendor:publish --tag=passport-migrations` & move to `database/migrations/tenant/` directory
## Shared keys
If you want to use the same keypair for all tenants, do the following.
- Don't use `passport:install`, use just `passport:keys`. The install command creates keys & two clients. Instead of creating clients centrally, create `Client`s manually in your [tenant database seeder]({{ $page->link('configuration/#seed-after-migration') }}).
## Tenant-specific keys
If you want to use a unique keypair for each tenant, do the following. (Note: The security benefit of doing this isn't probably that big, since you're likely already using the same `APP_KEY` for all tenants.)
There are multiple ways you can store & load tenant keys, but the most straightforward way is to store the keys in the [Tenant Storage]({{ $page->link('tenant-storage') }}) and load them into the `passport` configuration using the [Tenant Config]({{ $page->link('features/tenant-config') }}) feature:
- Uncomment the `TenantConfig` line in your `tenancy.features` config
- Add these keys to your `tenancy.storage_to_config_map` config:
```php
'storage_to_config_map' => [
'passport_public_key' => 'passport.public_key',
'passport_private_key' => 'passport.private_key',
],
```
And again, you need to create clients in your tenant database seeding process.

Some files were not shown because too many files have changed in this diff Show more