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 394 forks source link

Job using system connection instead of tenant #912

Open Bibendus83 opened 4 years ago

Bibendus83 commented 4 years ago

Description

I have some issues trying to execute a job on my tenants. I'm using qcod/laravel-app-settings library to easily implement custom settings for each tenant. However when my job tries to read the settings table from the tenant it seems that the connection is still set to system and the job fails with this error:

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'tenancy.settings' doesn't exist (SQL: select `val`, `name` from `settings` where `group` = default) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 42S02):
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'tenancy.settings' doesn't exist (SQL: select `val`, `name` from `settings` where `group` = default) at /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664, Doctrine\\DBAL\\Driver\\PDOException(code: 42S02): 
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'tenancy.settings' doesn't exist at /shared/httpd/my-app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:63, PDOException(code: 42S02): 
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'tenancy.settings' doesn't exist at /shared/httpd/my-app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:61)"}

Any idea on how to fix this?

If it can be of any help, I tried adding inside my job handle() all of the documentation suggested functions and 3 of 4 failed to obtain the current tenant website.

image


Information


ConfirmCheckoutPayments.php job

<?php

namespace App\Jobs;

use App\Models\Checkout;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;

class ConfirmCheckoutPayments implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $checkout;

    public $retryAfter = 60;

    public $tries = 10;

    public $website_id;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(int $websiteId, Checkout $checkout)
    {
        $this->website_id = $websiteId;
        $this->checkout = $checkout;
    }

    /**
     * Execute the job.
     * php artisan queue:work
     *
     * @return void
     */
    public function handle()
    {
        try {
            $this->checkout->setBookingStatus(Checkout::STATUS_COMPLETE);   // Throws error because setBookingStatus::__construct() fails due to system connection used instead of tenant
            $websiteId = app(\Hyn\Tenancy\Environment::class)->website()->id;   // Throws another error because app(\Hyn\Tenancy\Environment::class)->website() returns null
            SendMailPurchaseCompleted::dispatch(websiteId, $this->checkout);
        } catch (\Exception $e) {
            $this->release(60);

            throw $e;
        }
    }

    /**
     * The job failed to process.
     *
     * @param  \Exception  $exception
     * @return void
     */
    public function failed($exception = null)
    {
    }
}

tenancy.php config

<?php
use Hyn\Tenancy\Database\Connection;

return [
    /**
     * Random key used for tenant database user password
     */
    'key' => env('TENANCY_KEY', env('APP_KEY')),

    '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' => 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_TIME', 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' => env('TENANCY_UPDATE_APP_URL', false),

        /**
         * Root default FQDN where all subdomains are created by default
         */
        'root-fqdn' => env('TENANCY_ROOT_FQDN', false),
    ],
    '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' => 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.
         */
        // 'tenant-seed-class' => false,
//      eg an admin seeder under `app/Seeders/AdminSeeder.php`:
       'tenant-seed-class' => AdminTableSeeder::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,

        /**
         * Set of database privileges to give to the tenant database user.
         *
         * @info Useful in case your database restricts the privileges you
         *       can set (for example AWS RDS).
         * @info These privileges are only used in case tenant database users
         *       are set to be created.
         *
         * @info null by default means "ALL PRIVILEGES". Override with a list
         *       of privileges as a string, e.g. 'SELECT, UPDATE'.
         */
        'tenant-database-user-privileges' => null,

        /**
         * 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
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' => 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' => '/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' => 'apache2ctl -t',
                /**
                 * Action to run to reload the apache service.
                 *
                 * @info set to null to disable reloading.
                 */
                'reload' => 'apache2ctl graceful'
            ]
        ]
    ],

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

        /**
         * The php sock to be used.
         */
        'php-sock' => 'unix:/var/run/php/php7.3-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' => '/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' => '/etc/init.d/nginx configtest',
                /**
                 * Action to run to reload the nginx service.
                 *
                 * @info set to null to disable reloading.
                 */
                'reload' => '/etc/init.d/nginx reload'
            ]
        ]
    ]
];

.env file

APP_NAME="My App"
APP_ENV=local
APP_KEY=XXXXXXXXXXXXXXX
APP_DEBUG=true
APP_URL=https://www.my-app.loc:4430/
ASSET_URL=" "

LOCALE=it

LOG_CHANNEL=stack

DB_CONNECTION=system
DB_HOST=172.16.238.10
DB_PORT=3306
DB_DATABASE=tenant
DB_USERNAME=root
DB_PASSWORD=

# TENANCY_DEFAULT_CONNECTION=tenant
TENANCY_DRIVER=mysql
TENANCY_HOST=172.16.238.10
TENANCY_PORT=3306
TENANCY_DATABASE=tenancy
TENANCY_USERNAME=root
TENANCY_PASSWORD=XXXXXXXXXX
TENANCY_CACHE_TIME=false
TENANCY_AUTO_HOSTNAME_IDENTIFICATION=true
TENANCY_ABORT_WITHOUT_HOSTNAME=true
TENANCY_UPDATE_APP_URL=false    # setting it to true causes issues with url() and ports
TENANCY_DATABASE_DIVISION_MODE=database
TENANCY_ROOT_FQDN=my-app.loc
LIMIT_UUID_LENGTH_32=false

BROADCAST_DRIVER=log
CACHE_DRIVER=array
CACHE_EXPIRE=-1
QUEUE_CONNECTION=database
QUEUE_DRIVER=sync   # DEV
SESSION_DRIVER=file
SESSION_LIFETIME=120

...
Bibendus83 commented 4 years ago

Here is an additional test I just made. I tried adding the UsesTenantConnection trait to the library QCod\Settings\Setting\Setting.php that extends Model and the error is fixed. Obviously I can't change my vendor libraries but it should help with the debug of the issue.

I tried forcing the tenant connection with the setting in tenancy.php but it doesn't work

'force-tenant-connection-of-models' => [
    \QCod\Settings\Setting\Setting::class,
],
ArlonAntonius commented 4 years ago

Can you provide us with a full stacktrace of the error? :)

Bibendus83 commented 4 years ago

The error I reported on my first post is the only thing that appears on my logs and it doesn't give me the stack trace, how do you get a full stack trace on a queue job error?

Edit: I was able to manually get the stack trace catching the job exception, here it is:

local.INFO: 
#0 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(624): Illuminate\Database\Connection->runQueryCallback('select `val`, `...', Array, Object(Closure)) 
#1 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Connection.php(333): Illuminate\Database\Connection->run('select `val`, `...', Array, Object(Closure)) 
#2 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2130): Illuminate\Database\Connection->select('select `val`, `...', Array, true) 
#3 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2324): Illuminate\Database\Query\Builder->runSelect() 
#4 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2604): Illuminate\Database\Query\Builder->Illuminate\Database\Query\{closure}() 
#5 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php(2326): Illuminate\Database\Query\Builder->onceWithColumns(Array, Object(Closure)) 
#6 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php(717): Illuminate\Database\Query\Builder->pluck('val', 'name') 
#7 /shared/httpd/my-app/vendor/qcod/laravel-settings/src/Setting/SettingEloquentStorage.php(34): Illuminate\Database\Eloquent\Builder->pluck('val', 'name') 
#8 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Cache/Repository.php(418): QCod\Settings\Setting\SettingEloquentStorage->QCod\Settings\Setting\{closure}() 
#9 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Cache/CacheManager.php(357): Illuminate\Cache\Repository->rememberForever('app_settings.de...', Object(Closure)) 
#10 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(239): Illuminate\Cache\CacheManager->__call('rememberForever', Array) 
#11 /shared/httpd/my-app/vendor/qcod/laravel-settings/src/Setting/SettingEloquentStorage.php(35): Illuminate\Support\Facades\Facade::__callStatic('rememberForever', Array) 
#12 /shared/httpd/my-app/vendor/qcod/laravel-settings/src/Setting/SettingEloquentStorage.php(43): QCod\Settings\Setting\SettingEloquentStorage->all(false) 
#13 /shared/httpd/my-app/vendor/qcod/laravel-app-settings/src/Setting/AppSettings.php(60): QCod\Settings\Setting\SettingEloquentStorage->get('api_url', 'https://XXXXXXX...') 
#14 /shared/httpd/my-app/vendor/qcod/laravel-app-settings/src/helpers.php(24): QCod\AppSettings\Setting\AppSettings->get('api_url', 'UNSET_OAUTH_SER...') 
#15 /shared/httpd/my-app/app/Services/BookingApiConnection.php(23): setting('api_url', 'UNSET_OAUTH_SER...') 
#16 [internal function]: App\Services\BookingApiConnection->__construct(true) 
#17 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(825): ReflectionClass->newInstanceArgs(Array) 
#18 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(667): Illuminate\Container\Container->build('App\\Services\\Bo...') 
#19 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(615): Illuminate\Container\Container->resolve('App\\Services\\Bo...', Array) 
#20 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(767): Illuminate\Container\Container->make('App\\Services\\Bo...', Array) 
#21 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php(121): Illuminate\Foundation\Application->make('App\\Services\\Bo...', Array) 
#22 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php(733): app('App\\Services\\Bo...', Array) 
#23 /shared/httpd/my-app/app/Models/Checkout.php(143): resolve('App\\Services\\Bo...') 
#24 /shared/httpd/my-app/app/Jobs/ConfirmCheckoutPayments.php(67): App\Models\Checkout->setBookingStatus(6) 
#25 [internal function]: App\Jobs\ConfirmCheckoutPayments->handle() 
#26 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): call_user_func_array(Array, Array) 
#27 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() 
#28 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure)) 
#29 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(576): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL) 
#30 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(94): Illuminate\Container\Container->call(Array) 
#31 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\Bus\Dispatcher->Illuminate\Bus\{closure}(Object(App\Jobs\ConfirmCheckoutPayments)) 
#32 /shared/httpd/my-app/vendor/hyn/multi-tenant/src/Queue/DispatcherMiddleware.php(36): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Jobs\ConfirmCheckoutPayments)) 
#33 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(163): Hyn\Tenancy\Queue\DispatcherMiddleware->handle(Object(App\Jobs\ConfirmCheckoutPayments), Object(Closure)) 
#34 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Jobs\ConfirmCheckoutPayments)) 
#35 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(98): Illuminate\Pipeline\Pipeline->then(Object(Closure)) 
#36 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(49): Illuminate\Bus\Dispatcher->dispatchNow(Object(App\Jobs\ConfirmCheckoutPayments), false) 
#37 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(88): Illuminate\Queue\CallQueuedHandler->call(Object(Illuminate\Queue\Jobs\DatabaseJob), Array) 
#38 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(337): Illuminate\Queue\Jobs\Job->fire() 
#39 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(283): Illuminate\Queue\Worker->process('database', Object(Illuminate\Queue\Jobs\DatabaseJob), Object(Illuminate\Queue\WorkerOptions)) 
#40 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(236): Illuminate\Queue\Worker->runJob(Object(Illuminate\Queue\Jobs\DatabaseJob), 'database', Object(Illuminate\Queue\WorkerOptions)) 
#41 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(102): Illuminate\Queue\Worker->runNextJob('database', 'default', Object(Illuminate\Queue\WorkerOptions)) 
#42 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(86): Illuminate\Queue\Console\WorkCommand->runWorker('database', 'default') 
#43 [internal function]: Illuminate\Queue\Console\WorkCommand->handle() 
#44 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): call_user_func_array(Array, Array) 
#45 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() 
#46 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure)) 
#47 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Container/Container.php(576): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL) 
#48 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Console/Command.php(183): Illuminate\Container\Container->call(Array) 
#49 /shared/httpd/my-app/vendor/symfony/console/Command/Command.php(255): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle)) 
#50 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Console/Command.php(170): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle)) 
#51 /shared/httpd/my-app/vendor/symfony/console/Application.php(1001): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 
#52 /shared/httpd/my-app/vendor/symfony/console/Application.php(271): Symfony\Component\Console\Application->doRunCommand(Object(Illuminate\Queue\Console\WorkCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 
#53 /shared/httpd/my-app/vendor/symfony/console/Application.php(147): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 
#54 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Console/Application.php(90): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 
#55 /shared/httpd/my-app/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(133): Illuminate\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 
#56 /shared/httpd/my-app/artisan(37): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) 
#57 {main} [] []
Bibendus83 commented 4 years ago

Thanks to @fletch3555 on discord I fixed a secondary bug that happened tried to launch the following job SendMailPurchaseCompleted I was getting again the website id from Environment instead of using the already existing attribute $this->website_id

$this->checkout->setBookingStatus(Checkout::STATUS_COMPLETE);
$websiteId = app(\Hyn\Tenancy\Environment::class)->website()->id;
SendMailPurchaseCompleted::dispatch(websiteId, $this->checkout);

in

$this->checkout->setBookingStatus(Checkout::STATUS_COMPLETE);
SendMailPurchaseCompleted::dispatch($this->website_id, $this->checkout);

However the system connection error is still present.

fletch3555 commented 4 years ago

Based on that stacktrace, I'd guess the issue is with that other package. It appears to be using an Eloquent Builder instance instead of a Model class. Either way, we're unable to directly support another package. Take a look through their code and see if you can figure out what's going on.

Bibendus83 commented 4 years ago

The package uses an Eloquent Builder that calls for a Model: https://github.com/qcod/laravel-settings/blob/f87ee09837fcc7db2b0961f909d584b56a0f7575/src/Setting/SettingEloquentStorage.php

    public function all($fresh = false)
    {
        if ($fresh) {
            return $this->modelQuery()->pluck('val', 'name');
        }

        return Cache::rememberForever($this->getSettingsCacheKey(), function () {
            return $this->modelQuery()->pluck('val', 'name');
        });
    }

    // ...

    protected function modelQuery()
    {
        return $this->getSettingModel()->group($this->settingsGroupName);
    }

    // ...

    protected function getSettingModel()
    {
        return app('\QCod\Settings\Setting\Setting');
    }

and \QCod\Settings\Setting\Setting extends Model

Bibendus83 commented 4 years ago

Ok I found something. It appears that when the Setting model has a null connection defined. When Model->getConnection(null) is called, it goes through DatabaseManager->connection(null) and, as you can see from the screenshot, the default db connection from app['config']['database.default'] is assigned. When I call the settings library from a controller the function getDefaultConnection() returns tenant (see screenshot 1) image

However when I call the same library from my job getDefaultConnection() returns system (see screenshot 2) image

Bibendus83 commented 4 years ago

Ok, now I understood why it works only when called in a controller and not on my jobs. I had to force my tenant connection as hinted in the documentation. So I added this snippet of code in my AppServiceProvider.php

if ($fqdn = optional($env->hostname())->fqdn) {
    config(['database.default' => 'tenant']);
}

However app(Environment::class)->hostname() is null when called on a Job while works while calling it from a tenant controller.

Removing the snippet I get the error both calling the library from a controller or a job

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'tenancy.settings' doesn't exist (SQL: select `val`, `name` from `settings` where `group` = default)

So now the question is: why forcing the tenant connection in tenancy.php doesnt work?

'force-tenant-connection-of-models' => [
    \QCod\Settings\Setting\Setting::class,
],

I opened a new ticket #913 for that.

Bibendus83 commented 4 years ago

I'm still struggling trying to make the tenant connection work both on controllers and jobs. Considering force-tenant-connection-of-models doesn't work, I was able to find a hack to make it work at least on jobs.

I just added this line of code at the beginning of my job handle() function:

app()->resolveProvider(\Hyn\Tenancy\Providers\Tenants\ConnectionProvider::class)->overrideConnectionResolvers();

That's a horrible hack but that's the only way I found to make jobs work until force-tenant-connection-of-models is fixed (#751)