tenancy / multi-tenant

Run multiple websites using the same Laravel installation while keeping tenant specific data separated for fully independent multi-domain setups, previously github.com/hyn/multi-tenant
https://tenancy.dev
MIT License
2.56k stars 393 forks source link

Generate urls in queued e-mails not working for tenant routes #689

Closed ametad closed 5 years ago

ametad commented 5 years ago

Description

A queued e-mail is handled on the command line interface by the Laravel worker php artisan queue:work. I'd like the helper route('name.of.route') generate urls that are defined for the tenants in routes/tenants.php. These urls are needed for buttons in the mail.

Because of Tenancy, the context of where the urls are generated needs to be tenant-aware.


Actual behavior

Routes generated with the 'route()' helper are not generated and an error appears saying the named route is not found.

Expected behavior

Defined tenant routes (as described here https://laravel-tenancy.com/docs/hyn/5.3/fallback#tenant-routes-override) should be generated in a tenant-aware context. E.g. with the Hyn\Tenancy\Queue\TenantAwareJob or by running a command within the php artisan tenancy:run <custom-command> structure.


Information


tenancy.php config

<?php

/*
 * This file is part of the hyn/multi-tenant package.
 *
 * (c) Daniël Klabbers <daniel@klabbers.email>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @see https://laravel-tenancy.com
 * @see https://github.com/hyn/multi-tenant
 */

use Hyn\Tenancy\Database\Connection;

return [
    'models' => [
        /**
         * Specify different models to be used for the global, system database
         * connection. These are also used in their relationships. Models
         * used have to implement their respective contracts and
         * either extend the SystemModel or use the trait
         * UsesSystemConnection.
         */

        // Must implement \Hyn\Tenancy\Contracts\Hostname
        'hostname' => \Hyn\Tenancy\Models\Hostname::class,

        // Must implement \Hyn\Tenancy\Contracts\Website
        'website' => \Hyn\Tenancy\Models\Website::class
    ],
    /**
     * The package middleware. Removing a middleware here will disable it.
     * You can of course extend/replace them or add your own.
     */
    'middleware' => [
        // The eager identification middleware.
        \Hyn\Tenancy\Middleware\EagerIdentification::class,

        // The hostname actions middleware (redirects, https, maintenance).
        \Hyn\Tenancy\Middleware\HostnameActions::class,
    ],
    'website' => [
        /**
         * Each website has a short random hash that identifies this entity
         * to the application. By default this id is randomized and fully
         * auto-generated. In case you want to force your own logic for
         * when you need to have a better overview of the complete
         * tenant folder structure, disable this and implement
         * your own id generation logic.
         */
        'disable-random-id' => false,

        /**
         * The random Id generator is responsible for creating the hash as mentioned
         * above. You can override what generator to use by modifying this value
         * in the configuration.
         *
         * @warn This won't work if disable-random-id is true.
         */
        'random-id-generator' => Hyn\Tenancy\Generators\Uuid\ShaGenerator::class,

        /**
         * Enable this flag in case you're using a driver that does not support
         * database username or database name with a length of more than 32 characters.
         *
         * This should be enabled for MySQL, but not for MariaDB and PostgreSQL.
         */
        'uuid-limit-length-to-32' => env('LIMIT_UUID_LENGTH_32', false),

        /**
         * Specify the disk you configured in the filesystems.php file where to store
         * the tenant specific files, including media, packages, routes and other
         * files for this particular website.
         *
         * @info If not set, will revert to the default filesystem.
         * @info If set to false will disable all tenant specific filesystem auto magic
         *       like the config, vendor overrides.
         */
        'disk' => null,

        /**
         * Automatically generate a tenant directory based on the random id of the
         * website. Uses the above disk to store files to override system-wide
         * files.
         *
         * @info set to false to disable.
         */
        'auto-create-tenant-directory' => true,

        /**
         * Automatically rename the tenant directory when the random id of the
         * website changes. This should not be too common, but in case it happens
         * we automatically want to move files accordingly.
         *
         * @info set to false to disable.
         */
        'auto-rename-tenant-directory' => true,

        /**
         * Automatically deletes the tenant specific directory and all files
         * contained within.
         *
         * @see
         * @info set to true to enable.
         */
        'auto-delete-tenant-directory' => false,

        /**
         * Time to cache websites in minutes. Set to false to disable.
         */
        'cache' => env('TENANCY_CACHE', 10),
    ],
    'hostname' => [
        /**
         * If you want the multi tenant application to fall back to a default
         * hostname/website in case the requested hostname was not found
         * in the database, complete in detail the default hostname.
         *
         * @warn this must be a FQDN, these have no protocol or path!
         */
        'default' => env('TENANCY_DEFAULT_HOSTNAME'),
        /**
         * The package is able to identify the requested hostname by itself,
         * disable to get full control (and responsibility) over hostname
         * identification. The hostname identification is needed to
         * set a specific website as currently active.
         *
         * @see src/Jobs/HostnameIdentification.php
         */
        'auto-identification' => env('TENANCY_AUTO_HOSTNAME_IDENTIFICATION', true),

        /**
         * In case you want to have the tenancy environment set up early,
         * enable this flag. This will run the tenant identification
         * inside a middleware. This will eager load tenancy.
         *
         * A good use case is when you have set "tenant" as the default
         * database connection.
         */
        'early-identification' => env('TENANCY_EARLY_IDENTIFICATION', true),

        /**
         * Abort application execution in case no hostname was identified. This will throw a
         * 404 not found in case the tenant hostname was not resolved.
         */
        'abort-without-identified-hostname' => env('TENANCY_ABORT_WITHOUT_HOSTNAME', false),

        /**
         * Time to cache hostnames in minutes. Set to false to disable.
         */
        'cache' => env('TENANCY_CACHE', 10),

        /**
         * Automatically update the app.url configured inside Laravel to match
         * the tenant FQDN whenever a hostname/tenant was identified.
         *
         * This will resolve issues with password reset mails etc using the
         * correct domain.
         */
        'update-app-url' => true,
    ],
    'db' => [
        /**
         * The default connection to use; this overrules the Laravel database.default
         * configuration setting. In Laravel this is normally configured to 'mysql'.
         * You can set a environment variable to override the default database
         * connection to - for instance - the tenant connection 'tenant'.
         */
        'default' => env('TENANCY_DEFAULT_CONNECTION'),
        /**
         * Used to give names to the system and tenant database connections. By
         * default we configure 'system' and 'tenant'. The tenant connection
         * is set up automatically by this package.
         *
         * @see src/Database/Connection.php
         * @var system-connection-name The database connection name to use for the global/system database.
         * @var tenant-connection-name The database connection name to use for the tenant database.
         */
        'system-connection-name' => env('TENANCY_SYSTEM_CONNECTION_NAME', Connection::DEFAULT_SYSTEM_NAME),
        'tenant-connection-name' => env('TENANCY_TENANT_CONNECTION_NAME', Connection::DEFAULT_TENANT_NAME),

        /**
         * The tenant division mode specifies to what database websites will be
         * connecting. The default setup is to use a new database per tenant.
         * If using PostgreSQL, a new schema per tenant in the same database can
         * be setup, by optionally setting division mode to 'schema'.
         * In case you prefer to use the same database with a table prefix,
         * set the mode to 'prefix'.
         * To implement a custom division mode, set this to 'bypass'.
         *
         * @see src/Database/Connection.php
         */
        'tenant-division-mode' => env('TENANCY_DATABASE_DIVISION_MODE', 'database'),

        /**
         * The database password generator takes care of creating a valid hashed
         * string used for tenants to connect to the specific database. Do
         * note that this will only work in 'division modes' that set up
         * a connection to a separate database.
         */
        'password-generator' => Hyn\Tenancy\Generators\Database\DefaultPasswordGenerator::class,

        /**
         * The tenant migrations to be run during creation of a tenant. Specify a directory
         * to run the migrations from. If specified these migrations will be executed
         * whenever a new tenant is created.
         *
         * @info set to false to disable auto migrating.
         *
         * @warn this has to be an absolute path, feel free to use helper methods like
         * base_path() or database_path() to set this up.
         */
        'tenant-migrations-path' => false,

        /**
         * The default Seeder class used on newly created databases and while
         * running artisan commands that fire seeding.
         *
         * @info requires tenant-migrations-path in order to seed newly created websites.
         * @info seeds stored in `database/seeds/tenants` need to be configured in your composer.json classmap.
         *
         * @warn specify a valid fully qualified class name.
         */
        'tenant-seed-class' => false,
//      eg an admin seeder under `app/Seeders/AdminSeeder.php`:
//        'tenant-seed-class' => App\Seeders\AdminSeeder::class,

        /**
         * Automatically generate a tenant database based on the random id of the
         * website.
         *
         * @info set to false to disable.
         */
        'auto-create-tenant-database' => false,

        /**
         * Automatically generate the user needed to access the database.
         *
         * @info Useful in case you use root or another predefined user to access the
         *       tenant database.
         * @info Only creates in case tenant databases are set to be created.
         *
         * @info set to false to disable.
         */
        'auto-create-tenant-database-user' => true,

        /**
         * Automatically rename the tenant database when the random id of the
         * website changes. This should not be too common, but in case it happens
         * we automatically want to move databases accordingly.
         *
         * @info set to false to disable.
         */
        'auto-rename-tenant-database' => true,

        /**
         * Automatically deletes the tenant specific database and all data
         * contained within.
         *
         * @info set to true to enable.
         */
        'auto-delete-tenant-database' => env('TENANCY_DATABASE_AUTO_DELETE', false),

        /**
         * Automatically delete the user needed to access the tenant database.
         *
         * @info Set to false to disable.
         * @info Only deletes in case tenant database is set to be deleted.
         */
        'auto-delete-tenant-database-user' => env('TENANCY_DATABASE_AUTO_DELETE_USER', false),

        /**
         * Define a list of classes that you wish to force onto the tenant or system connection.
         * The connection will be forced when the Model has booted.
         *
         * @info Useful for overriding the connection of third party packages.
         */
        'force-tenant-connection-of-models' => [
//            App\User::class
        ],
        'force-system-connection-of-models' => [
//            App\User::class
        ],
    ],

    /**
     * Global tenant specific routes.
     * Making it easier to distinguish between landing and tenant routing.
     *
     * @info only works with `tenancy.hostname.auto-identification` or identification happening
     *       before the application is booted (eg inside middleware or the register method of
     *       service providers).
     */
    'routes' => [
        /**
         * Routes file to load whenever a tenant was identified.
         *
         * @info Set to false or null to disable.
         */
        'path' => base_path('routes/tenants.php'),

        /**
         * Set to true to flush all global routes before setting the routes from the
         * tenants.php routes file.
         */
        'replace-global' => false,
    ],

    /**
     * Folders configuration specific per tenant.
     * The following section relates to configuration to files inside the tenancy/<uuid>
     * tenant directory.
     */
    'folders' => [
        'config' => [
            /**
             * Merge configuration files from the config directory
             * inside the tenant directory with the global configuration files.
             */
            'enabled' => true,

            /**
             * List of configuration files to ignore, preventing override of crucial
             * application configurations.
             */
            'blacklist' => ['database', 'tenancy', 'webserver'],
        ],
        'routes' => [
            /**
             * Allows adding and overriding URL routes inside the tenant directory.
             */
            'enabled' => true,

            /**
             * Prefix all tenant routes.
             */
            'prefix' => null,
        ],
        'trans' => [
            /**
             * Allows reading translation files from a trans directory inside
             * the tenant directory.
             */
            'enabled' => true,

            /**
             * Will override the global translations with the tenant translations.
             * This is done by overriding the laravel default translator with the new path.
             */
            'override-global' => true,

            /**
             * In case you disabled global override, specify a namespace here to load the
             * tenant translation files with.
             */
            'namespace' => 'tenant',
        ],
        'vendor' => [
            /**
             * Allows using a custom vendor (composer driven) folder inside
             * the tenant directory.
             */
            'enabled' => true,
        ],
        'media' => [
            /**
             * Mounts the assets directory with (static) files for public use.
             */
            'enabled' => true,
        ],
        'views' => [
            /**
             * Enables reading views from tenant directories.
             */
            'enabled' => true,

            /**
             * Specify a namespace to use with which to load the views.
             *
             * @eg setting `tenant` will allow you to use `tenant::some.blade.php`
             * @info set to null to add to the global namespace.
             */
            'namespace' => null,

            /**
             * If `namespace` is set to null (thus using the global namespace)
             * make it override the global views. Disable by setting to false.
             */
            'override-global' => true,
        ]
    ]
];

webserver.php config

<?php

/*
 * This file is part of the hyn/multi-tenant package.
 *
 * (c) Daniël Klabbers <daniel@klabbers.email>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @see https://laravel-tenancy.com
 * @see https://github.com/hyn/multi-tenant
 */

return [

    /**
     * Apache2 is one of the most widely adopted webserver packages available.
     *
     * @see http://httpd.apache.org/docs/
     * @see https://www.digitalocean.com/community/tutorials/how-to-install-linux-apache-mysql-php-lamp-stack-on-ubuntu
     */
    'apache2' => [
        /**
         * Whether the integration with Apache2 is currently active.
         */
        'enabled' => env('TENANCY_WEBSERVER_APACHE2', false),

        /**
         * Define the ports of your Apache service.
         */
        'ports' => [
            /**
             * HTTP, non-SSL port.
             *
             * @default 80
             */
            'http' => 80,
            /**
             * HTTPS, SSL port.
             *
             * @default 443
             */
            'https' => 443
        ],

        /**
         * The generator taking care of hooking into the Apache services and files.
         */
        'generator' => \Hyn\Tenancy\Generators\Webserver\Vhost\ApacheGenerator::class,

        /**
         * The view that holds the vhost configuration template.
         */
        'view' => 'tenancy.generators::webserver.apache.vhost',

        /**
         * Specify the disk you configured in the filesystems.php file where to store
         * the tenant vhost configuration files.
         *
         * @info If not set, will revert to the default filesystem.
         */
        'disk' => null,

        'paths' => [

            /**
             * Location where vhost configuration files can be found.
             */
            'vhost-files' => [
                '/etc/apache2/sites-enabled/'
            ],

            /**
             * Actions to run to work with the Apache2 service.
             */
            'actions' => [
                /**
                 * Action that asserts Apache2 is installed.
                 */
                'exists' => 'sudo /etc/init.d/apache2',
                /**
                 * Action to run to test the apache configuration.
                 *
                 * @set to a boolean to force the response of the test command.
                 * @info true succeeds, false fails
                 */
                'test-config' => 'sudo apache2ctl -t',
                /**
                 * Action to run to reload the apache service.
                 *
                 * @info set to null to disable reloading.
                 */
                'reload' => 'sudo apache2ctl graceful'
            ]
        ]
    ],

    /**
     * Nginx webserver support.
     *
     * @see http://nginx.org
     */
    'nginx' => [
        /**
         * Whether the integration with nginx is currently active.
         */
        'enabled' => env('TENANCY_WEBSERVER_NGINX', false),

        /**
         * The php sock to be used.
         */
        'php-sock' => 'unix:/var/run/php/php7.1-fpm.sock',

        /**
         * Define the ports of your nginx service.
         */
        'ports' => [
            /**
             * HTTP, non-SSL port.
             *
             * @default 80
             */
            'http' => 80,
            /**
             * HTTPS, SSL port.
             *
             * @default 443
             */
            'https' => 443
        ],

        /**
         * The generator taking care of hooking into the nginx services and files.
         */
        'generator' => \Hyn\Tenancy\Generators\Webserver\Vhost\NginxGenerator::class,

        /**
         * The view that holds the vhost configuration template.
         */
        'view' => 'tenancy.generators::webserver.nginx.vhost',

        /**
         * Specify the disk you configured in the filesystems.php file where to store
         * the tenant vhost configuration files.
         *
         * @info If not set, will revert to the default filesystem.
         */
        'disk' => null,

        'paths' => [

            /**
             * Location where vhost configuration files can be found.
             */
            'vhost-files' => [
                '/etc/nginx/sites-enabled/'
            ],

            /**
             * Actions to run to work with the Nginx service.
             */
            'actions' => [
                /**
                 * Action that asserts nginx is installed.
                 */
                'exists' => 'sudo /etc/init.d/nginx',
                /**
                 * Action to run to test the nginx configuration.
                 *
                 * @info set to a boolean to force the response of the test command.
                 *  true succeeds, false fails
                 */
                'test-config' => 'sudo /etc/init.d/nginx configtest',
                /**
                 * Action to run to reload the nginx service.
                 *
                 * @info set to null to disable reloading.
                 */
                'reload' => 'sudo /etc/init.d/nginx reload'
            ]
        ]
    ]
];

.env

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:xkytOFtgV27hFAJLKc6aX19cmJurwUZpkebUqNZNta8=
APP_DEBUG=true
APP_LOG=daily
APP_LOG_LEVEL=debug
APP_URL=https://laravel-app.test

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

TENANCY_DATABASE=tenancy
TENANCY_USERNAME=homestead
TENANCY_PASSWORD=secret
LIMIT_UUID_LENGTH_32=true
TENANCY_DEFAULT_CONNECTION=tenant
TENANCY_EARLY_IDENTIFICATION=true
TENANCY_ABORT_WITHOUT_HOSTNAME=false
TENANCY_WEBSERVER_APACHE2=false
TENANCY_WEBSERVER_NGINX=true
TENANCY_CACHE=false

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=beanstalkd
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

routes/web.php

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

/*
 * NOTICE:
 * The tenant website routes are defined in routes/tenants.php.
 */

/**
 * Admin pages
 *
 * Must be defined first.
 */
Route::domain(env('APP_URL'))->group(function () {

    Route::get('/', function () {
        return 'Admin page...';
    });

});

/**
 * Landing pages are defined here.
 *
 * If the tenant website is not found, the landing pages are shown.
 */
Route::get('/', function () {

    return 'Landing page...';
});

routes/tenants.php

<?php

/*
|--------------------------------------------------------------------------
| Tenant app Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web and api routes for your tenant application.
|
*/

/**
 * Web routes of tenant
 */
Route::middleware('web')->namespace('App\Http\Controllers')->group(function () {

    Auth::routes(['verify' => true]);

    // Invitee register
    Route::get('invitee/register/{email}/{token}', 'Auth\\InviteeRegisterController@showRegistrationForm')->name('invitee.register');
    Route::post('invitee/register/{email}/{token}', 'Auth\\InviteeRegisterController@register');
    // Invitee login
    Route::get('invitee/login/{email}/{token}', 'Auth\\InviteeLoginController@showLoginForm')->name('invitee.login');
    Route::post('invitee/login/{email}/{token}', 'Auth\\InviteeLoginController@login');

});

/**
 * API routes of tenant
 */
Route::prefix('api')->middleware('api')->namespace('App\Http\Controllers')->group(function () {

    //

});

Tenant screenshot

Screenshot of a correct working tenant auth route, trough http protocol:

image

ametad commented 5 years ago

Forgot to include something.

This is where I cannot get the helper route() to generate correct tenant urls:

<?php

namespace App\Mail;

use Hyn\Tenancy\Contracts\Website;
use Hyn\Tenancy\Queue\TenantAwareJob;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\URL;

class InvitationMade extends Mailable implements ShouldQueue
{
    use Queueable,TenantAwareJob;

    /**
     * @var Website
     */
    public $website;
    /**
     * @var string
     */
    public $fqdn;
    /**
     * @var string
     */
    private $token;
    /**
     * @var string
     */
    private $email;

    /**
     * Create a new message instance.
     *
     * @param Website $website
     * @param string $email
     * @param string $token
     */
    public function __construct(Website $website, string $email, string $token)
    {
        $this->website = $website;
        $this->token = $token;
        $this->email = $email;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->markdown('emails.invitation')
            //->from($this->website->company->email) // todo
            ->subject('Uitnodiging ' . $this->website->name)
            ->with([
                'invitee_register_url' => $this->registerUrl(),
                'invitee_login_url' => $this->loginUrl(),
            ]);
    }

    protected function registerUrl(): string
    {
        if (! $this->configTenantUrl()) {
            return null;
        }

        return "{$this->fqdn}/invitee/register/{$this->email}/{$this->token}";
//        return route('invitee.register', ['email' => $this->email, 'token' => $this->token]);
    }

    protected function loginUrl(): string
    {
        if (! $this->configTenantUrl()) {
            return null;
        }

        return "{$this->fqdn}/invitee/login/{$this->email}/{$this->token}";
//        return route('invitee.login', ['email' => $this->email, 'token' => $this->token]);
    }

    protected function configTenantUrl(): bool
    {
        if ($hostname = $this->findHostname()) {

            $prefix = $hostname->force_https ? 'https://' : 'http://';
            $fqdn = "{$prefix}{$hostname->fqdn}";

            URL::forceRootUrl($fqdn);
            config(['app.url' => $fqdn]);
            $this->fqdn = $fqdn;

            return true;
        }

        return false;
    }

    /**
     * @return mixed
     */
    protected function findHostname()
    {
        return $this->website
            ->hostnames()
            ->first();
    }
}
luceos commented 5 years ago

You are assuming because you're sending a Website model into the job that the TenantAwareJob trait automatically picks that up. That's not the case, what happens is that the trait will check whether a Tenant Website is currently active, if that is the case that Tenant is used when the Job is deserialised. When dispatching the job you will have to force the job to use the Website you wanted to sent as argument, so:

dispatch(new InvitationMade($website, 'foo@bar.com', '123abc'));

becomes:

dispatch((new InvitationMade($website, 'foo@bar.com', '123abc'))->onTenant($website));

as per docs: https://laravel-tenancy.com/docs/hyn/5.3/queues


I hope this helps.

Also there are some issues with the root URL being forced which will be resolved in a future version.

ametad commented 5 years ago

Thank you for your time @luceos,

The Website $website in the constructor of InvitationMade is there for another reason, it is needed in the view of the email to display some info.

Because this is an email, I use this syntax to send it:

Mail::to($email)->send(new InvitationMade($website, $email, $token));

And InvitationMade implements Illuminate\Contracts\Queue\ShouldQueue so it is send trough the queue.

Sending the e-mail is initiated trough an Artisan Command, therefore I run it within Tenancy context with: artisan tenancy:run --tenant=1 invite:user --argument="email=foo@example.com"

Should the TenantAwareJob trait in the mailable not pick up the context of the now 'active' tenant?

luceos commented 5 years ago

Have you tried what I suggested?

ametad commented 5 years ago

I misread your suggestion perhaps..? Because I already use the Hyn\Tenancy\Queue\TenantAwareJob trait the active tenant is automatically set. The tenant is set on the command line with artisan tenancy:run --tenant=1.

But to answer your question: no I havent tried setting the tenant manually with ->onTenant($website).

I will try to set it manually and report back here.

ametad commented 5 years ago

Also manually call onTenant($website) does NOT do the trick...

I have been trying a lot of things and it keeps boiling down to:

Perhaps this has something to do with #686 because there the routes (defined 'routes/tenants.php') are also not resolved on cli.

Test

Mail app/Mail/InvitationMade.php:

<?php

namespace App\Mail;

use App\Contracts\CanBeInvited;
use Hyn\Tenancy\Queue\TenantAwareJob;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Support\Facades\URL;
use Hyn\Tenancy\Contracts\Website.php;

class InvitationMade extends Mailable
{
    use Queueable,
        TenantAwareJob;

    /**
     * @var Website
     */
    public $website;
    /**
     * @var string
     */
    public $url;
    /**
     * @var CanBeInvited
     */
    public $data;
    /**
     * @var string
     */
    private $token;
    /**
     * @var string
     */
    private $email;
    /**
     * @var int
     */
    private $invitation_id;

    /**
     * Create a new message instance.
     *
     * @param Website $website
     * @param int $invitation_id
     * @param string $email
     * @param string $token
     * @param CanBeInvited|null $data
     */
    public function __construct(Website $website, int $invitation_id, string $email, string $token, CanBeInvited $data = null)
    {
        $this->website = $website;
        $this->token = $token;
        $this->email = $email;
        $this->invitation_id = $invitation_id;
        $this->data = $data;

        $this->onTenant($website);
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->markdown('emails.invitation')
            //->from($this->website->company->email) // todo
            ->subject('Uitnodiging ' . $this->website->name)
            ->with([
                'invitee_register_url' => $this->registerUrl(),
                'invitee_login_url' => $this->loginUrl(),
            ]);
    }

    protected function registerUrl(): string
    {
        if (!$this->configTenantUrl()) {
            return null;
        }

        return route('invitee.register', ['invitation_id' => $this->invitation_id, 'email' => $this->email, 'token' => $this->token]);
    }

    protected function configTenantUrl(): bool
    {
        if ($this->url) {
            return true;
        }

        if ($hostname = $this->findHostname()) {

            $prefix = $hostname->force_https ? 'https://' : 'http://';
            $url = "{$prefix}{$hostname->fqdn}";

            URL::forceRootUrl($url);
            config(['app.url' => $url]);
            $this->url = $url;

            return true;
        }

        return false;
    }

    /**
     * @return mixed
     */
    protected function findHostname()
    {
        return $this->website
            ->hostnames()
            ->first();
    }

    protected function loginUrl(): string
    {
        if (!$this->configTenantUrl()) {
            return null;
        }

        return route('invitee.login', ['invitation_id' => $this->invitation_id, 'email' => $this->email, 'token' => $this->token]);
    }
}

(Notice: ShouldQueue is removed. This mail will not automatically send trough a queue)


Called directly in HTTP context:

routes/tenants.php


use App\Mail\InvitationMade;

Route::middleware('web')->namespace('App\Http\Controllers')->group(function () {

    // todo remove testing
    Route::get('mail-direct', function () {
        $website = Tenancy::website();

        // SEND
        Mail::to('test@example.com')->send(new InvitationMade($website, 1, 'test@example.com', 'secret-code-123', null));
    });
    Route::get('mail-queued', function () {
        $website = Tenancy::website();

        //QUEUE
        Mail::to('test@example.com')->queue(new InvitationMade($website, 1, 'test@example.com', 'secret-code-123', null));
    });

    Auth::routes(['verify' => true]);

    // Invitee register
    Route::get('invitee/register/{invitation_id}/{email}/{token}', 'Auth\\InviteeRegisterController@showRegistrationForm')->name('invitee.register');
    Route::post('invitee/register/{invitation_id}/{email}/{token}', 'Auth\\InviteeRegisterController@register');
    // Invitee login
    Route::get('invitee/login/{invitation_id}/{email}/{token}', 'Auth\\InviteeLoginController@showLoginForm')->name('invitee.login');
    Route::post('invitee/login/{invitation_id}/{email}/{token}', 'Auth\\InviteeLoginController@login');

HTTP context

In browser hit /mail-direct : works perfectly, routes are resolved correctly!

image

CLI context

In browser hit /mail-queued : Error!

image

From the logs:

InvalidArgumentException: Route [invitee.register] not defined
ametad commented 5 years ago

@luceos Would you be so kind to look over a demo I've made to reproduce my problem?

https://github.com/ametad/laravel-app-multi-tenant-demo

Please let me know if you if you want to know something I can assist you with.

Regards.

luceos commented 5 years ago

Let me know whether this problem persist with 5.4.0-beta.2; it makes all Queue jobs tenant aware.

ametad commented 5 years ago

I will make some time in the next days. Thank you for letting me know!

ametad commented 5 years ago

@luceos Yes it works! This is very nice. Thank you for digging into this.

Any estimate on the 5.4 release? Succes.