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.55k stars 392 forks source link

URL Not Generating Properly #641

Open DeveloperOnCall opened 5 years ago

DeveloperOnCall commented 5 years ago

Description

When running an artisan job that loops through all web sites - the URL within the results (an email) is set to localhost. I do have the 'update-app-url' set to true.

$repository = app(WebsiteRepository::class);
        $websites = $repository->all();
        //dd( $websites );
        foreach( $websites as $siter ) {

            $environment = \App::make(\Hyn\Tenancy\Environment::class);
            $environment->tenant($siter);
// Processes the message etc

//LOOPS FOR USERS

    \Mail::to($recipient->email)->send(new DailyEmail($objDailyMail));

// END LOOP

}

It is using a Laravel Mailer to generate an email and then 'jdavidbakr/mail-tracker' (2.1) to replace links and add a tracking pixel to the email.


Actual behavior

Sets the tracking URL to http://localhost/ throughout the entire email as well as the tracking pixel and 'view' event on that email never fires as a result

Expected behavior

Should use the app.url to the current active tenant


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\Customer
        'customer' => \Hyn\Tenancy\Models\Customer::class,

        // 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', true),

        /**
         * 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,
        'auto-delete-tenant-directory' => env('AUTO_DELETE_TENANT_DIRECTORY', false),

        /**
         * Time to cache websites in minutes. Set to false to disable.
         */
        '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' => true,

        /**
         * Time to cache hostnames in minutes. Set to false to disable.
         */
        '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.
         * In case you prefer to use the same database with a table prefix,
         * set the mode to 'prefix'.
         *
         * @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' => database_path('migrations/tenant'),

        /**
         * 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.
         * @example App\Seeders\AdminSeeder::class
         */
        //'tenant-seed-class' => false,
        'tenant-seed-class' => App\Seeder\TenantSeeder::class,

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

        /**
         * 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' => false, //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' => [
            /**
             * Adds the vendor directory of the tenant inside the application.
             */
            '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


Error log

luceos commented 5 years ago

DailyEmail most likely needs to implement the TenantAwareJob trait, see: https://laravel-tenancy.com/docs/hyn/5.3/queues

DeveloperOnCall commented 5 years ago

I tried a few things - including your recommendation. Not luck yet.

Curious - I replaced the method that I am using with the following - just to see if the url('/') is working properly up to that point.

$repository = app(WebsiteRepository::class);
        $websites = $repository->all();
        //dd( $websites ); // THIS WORKS I am given the first web site details
        foreach( $websites as $siter ) {

            $environment = \App::make(\Hyn\Tenancy\Environment::class);
            $environment->tenant($siter);
            dd( url('/'), 'There');

    }

Which very strangely returns:


    "http://localhost"
    "There"

From what I understand I should be getting the URL to the current (first iteration) web site's url.

What am I doing wrong here?

Thanks, Daniel

bkintanar commented 5 years ago

Humor me and try using config('app.url') instead of url

DeveloperOnCall commented 5 years ago

Hi @bkintanar thanks for chiming in here..

I gave that a try and am returned the same 'localhost' value.

D

bkintanar commented 5 years ago

Ok. what does your TENANCY_AUTO_HOSTNAME_IDENTIFICATION in your .env looks like?

DeveloperOnCall commented 5 years ago

What makes it a little more confusing is that we are using the tenant.database at that point - when I dump a value from the db - it is using the first web sites database properly.

Problem just seems to be the URL.

To your other question - I do not have that set in .env rather setting the default in the config/tenancy.php


   'auto-identification' => env('TENANCY_AUTO_HOSTNAME_IDENTIFICATION', true),

D

bkintanar commented 5 years ago

Well the thing is that the setting of the update-app-url happens in the HostnameActions middleware, and only if auto-identification is enabled. Which you claim it's set to true.

DeveloperOnCall commented 5 years ago

For argument's sake I set it in .env here are the related entries


TENANCY_DATABASE_AUTO_DELETE=true
TENANCY_DATABASE_AUTO_DELETE_USER=true
TENANCY_DEFAULT_CONNECTION=system

AUTO_DELETE_TENANT_DIRECTORY=true
TENANCY_EARLY_IDENTIFICATION=true
TENANCY_AUTO_HOSTNAME_IDENTIFICATION=true

Still returning localhost...

bkintanar commented 5 years ago

Looks like this is a bug then. HostnameActions is a middleware so it's only executed when an http request is made.

@luceos ?

DeveloperOnCall commented 5 years ago

Could it be the loop? I hacked the src (recommended on an issue from some time ago - to be able to loop through all sites (in order to run a job on each site)


        $repository = app(WebsiteRepository::class);
        $websites = $repository->all();

Which does work to loop all sites. Then using the following to set the environment etc


   foreach( $websites as $siter ) {
            $environment = \App::make(\Hyn\Tenancy\Environment::class);
            $environment->tenant($siter); 
            // This works to give me the database (and Storage I believe)
   }
bkintanar commented 5 years ago

I don't believe it's the loop since you're able to get the correct hostname out of it. I believe it's (app.url) not being set at all.

DeveloperOnCall commented 5 years ago

Just to further help any debug here -- I made this change


$repository = app(WebsiteRepository::class);
        $websites = $repository->all();
        //dd( $websites );
        foreach( $websites as $siter ) {
            $environment = \App::make(\Hyn\Tenancy\Environment::class);
            $environment->tenant($siter);

            // Get current Hostname
            $hostname  = app(\Hyn\Tenancy\Environment::class)->hostname();

            // Get FQDN (Fully-Qualified Domain Name) by current hostname
            $fqdn      = $hostname->fqdn;
            dd( config('app.url'), $fqdn);

        }

Strangely I am not getting the FQDN either. This is what is dumped


   "http://localhost"
     null
bkintanar commented 5 years ago

You cant get the hostname that way.

$env = app(Environment::class);
$env->tenant($tenant);

$hostname = $env->hostname();

Can you try this code above?

DeveloperOnCall commented 5 years ago

No change..


$repository = app(WebsiteRepository::class);
        $websites = $repository->all();
        //dd( $websites );
        foreach( $websites as $siter ) {
            //$environment = \App::make(\Hyn\Tenancy\Environment::class);
            //$environment->tenant($siter);

            $env = \App(\Hyn\Tenancy\Environment::class);
            $env->tenant($siter);

            $hostname = $env->hostname();
            // Get current Hostname

            dd( $hostname, 'bkintanar Host' );

            // Get FQDN (Fully-Qualified Domain Name) by current hostname
            $fqdn      = $hostname->fqdn;

            dd( config('app.url'), $fqdn);

        }

Here is the dump


   null
   "bkintanar Host"
DeveloperOnCall commented 5 years ago

When I dd($siter) in the above loop...


Hyn\Tenancy\Models\Website {#8910
  #connection: "system"
  #table: null
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #attributes: array:7 [
    "id" => 22
    "uuid" => "e2b798f1a01e43f1832d3bfb051298aa"
    "customer_id" => 22
    "created_at" => "2018-05-29 16:55:19"
    "updated_at" => "2018-05-29 16:55:19"
    "deleted_at" => null
    "managed_by_database_connection" => null
  ]
  #original: array:7 [
    "id" => 22
    "uuid" => "e2b798f1a01e43f1832d3bfb051298aa"
    "customer_id" => 22
    "created_at" => "2018-05-29 16:55:19"
    "updated_at" => "2018-05-29 16:55:19"
    "deleted_at" => null
    "managed_by_database_connection" => null
  ]
  #changes: []
  #casts: []
  #dates: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  +timestamps: true
  #hidden: []
  #visible: []
  #fillable: []
  #guarded: array:1 [
    0 => "*"
  ]
  #forceDeleting: false
}

Is this normal?

D

DeveloperOnCall commented 5 years ago

For clarity sake - I only made the one change to the multi-tenant core.. (Hyn\Tenancy\Repositories\WebsiteRepository)


      // Hack to enable looping through all websites (used to perform a job on all sites)
      public function all() {
          return $this->website->all();
      }
mauricius commented 5 years ago

I'm using version 5.1 in my project and I have the same problem. url('/') returns always the value stored inside the .env file under the APP_URL key, even though I'm successfully switching tenants.

I finally managed to solve it by forcing Laravel's UrlGenerator class to use a custom root url:

app(UrlGenerator::class)->forceRootUrl(request()->getScheme() . '://' . $hostname->fqdn);
DeveloperOnCall commented 5 years ago

@mauricius Thanks -- but I am not even able to access the $hostname - digging into that at the moment.

D

luceos commented 5 years ago

@mauricius interesting method call, we might be able to use that in the package as well.. Feel free to PR that if you like.

DeveloperOnCall commented 5 years ago

@luceos you’re right the comment on the other issue is related. Is this a bug or still a support problem or something I’m doing wrong?

ametad commented 5 years ago

Curious: what you get on command line interface when trying:

$ php artisan tenancy:run --tenant=1 route:list

I am having also problems with generating urls. Indeed, with 'URL::forceRootUrl()` the url can be set on command line interface by first finding the first() hostname of the tenant.

But generating an url for a named route doesn't work on cli, in a Command e.g.:

echo route('my.custom-route');

In a Controller the code does work perfectly!

Could you tell me if you can list your tenant routes on the command line?

ametad commented 5 years ago

Sorry, maybe I hijacked your issue a little with my question... That wasn't my intention. I will open a new issue myself.

finalblast commented 5 years ago

+1