├── src ├── stubs │ └── config.stub ├── config │ └── domain.php ├── Foundation │ ├── Bootstrap │ │ └── DetectDomain.php │ ├── Console │ │ ├── DomainCommand.php │ │ ├── RemoveDomainCommand.php │ │ ├── ListDomainCommand.php │ │ ├── Kernel.php │ │ ├── UpdateEnvDomainCommand.php │ │ ├── DomainCommandTrait.php │ │ └── AddDomainCommand.php │ ├── Configuration │ │ └── ApplicationBuilder.php │ ├── Http │ │ └── Kernel.php │ ├── helpers.php │ ├── Providers │ │ └── DomainConsoleServiceProvider.php │ ├── DomainDetector.php │ └── Application.php ├── Horizon │ ├── WorkerCommandString.php │ ├── SupervisorCommandString.php │ ├── Console │ │ ├── TimeoutCommand.php │ │ ├── HorizonCommand.php │ │ └── SupervisorCommand.php │ ├── SupervisorProcess.php │ ├── MasterSupervisorCommands │ │ └── AddSupervisor.php │ ├── HorizonApplicationServiceProvider.php │ ├── QueueCommandString.php │ ├── ProvisioningPlan.php │ └── SupervisorOptions.php ├── Console │ └── Application.php └── Queue │ ├── QueueServiceProvider.php │ ├── Console │ └── ListenCommand.php │ ├── ListenerOptions.php │ └── Listener.php ├── laravel-multidomain.png ├── .gitignore ├── tests ├── app │ ├── Http │ │ ├── Middleware │ │ │ ├── EncryptCookies.php │ │ │ ├── VerifyCsrfToken.php │ │ │ ├── TrimStrings.php │ │ │ ├── CheckForMaintenanceMode.php │ │ │ └── RedirectIfAuthenticated.php │ │ └── Kernel.php │ ├── Console │ │ ├── Commands │ │ │ ├── NameCommand.php │ │ │ └── QueuePushCommand.php │ │ └── Kernel.php │ ├── Jobs │ │ └── AppNameJob.php │ └── TestServiceProvider.php ├── routes │ └── web.php ├── work_in_progress │ ├── ArtisanSubfolderTestCase.php │ └── ArtisanTestCase.php ├── .env.example ├── resources │ └── views │ │ └── welcome.blade.php ├── artisan ├── artisan_sub ├── database │ └── migrations │ │ ├── 0001_01_01_000001_create_cache_table.php │ │ ├── 0001_01_01_000000_create_users_table.php │ │ └── 0001_01_01_000002_create_jobs_table.php ├── src │ ├── CommandsSubfolderTestCase.php │ ├── HttpSubfolderTestCase.php │ ├── CommandsTestCase.php │ └── HttpTestCase.php ├── bootstrap │ └── app.php ├── README.md └── config │ ├── queue.php │ └── app.php ├── LICENSE ├── phpunit.xml.bak ├── phpunit.xml ├── composer.json ├── .phpunit.cache └── test-results ├── README.md └── CHANGELOG.md /src/stubs/config.stub: -------------------------------------------------------------------------------- 1 | '.env', 5 | 'storage_dirs' => [ 6 | 'app' => [ 7 | 'public' => [ 8 | 9 | ], 10 | ], 11 | 'framework' => [ 12 | 'cache' => [ 13 | ], 14 | 'testing' => [ 15 | ], 16 | 'sessions' => [ 17 | ], 18 | 'views' => [ 19 | ], 20 | ], 21 | 'logs' => [ 22 | ], 23 | ], 24 | 'domains' => [ 25 | ], 26 | ]; -------------------------------------------------------------------------------- /tests/routes/web.php: -------------------------------------------------------------------------------- 1 | laravelAppPath = __DIR__ . '/../../vendor/orchestra/testbench-core/laravel'; 18 | $this->laravelEnvPath = $this->laravelAppPath . DIRECTORY_SEPARATOR . 'envs'; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/Foundation/Bootstrap/DetectDomain.php: -------------------------------------------------------------------------------- 1 | detectDomain(); 18 | 19 | //Overrides the storage path if the domain stoarge path exists 20 | //$app->useStoragePath($app->domainStoragePath()); 21 | 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Horizon/WorkerCommandString.php: -------------------------------------------------------------------------------- 1 | check()) { 22 | return redirect(RouteServiceProvider::HOME); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Horizon/Console/TimeoutCommand.php: -------------------------------------------------------------------------------- 1 | option('domain'))->plan; 24 | 25 | $this->line(collect($plan[$this->argument('environment')] ?? [])->max('timeout') ?? 60); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Foundation/Console/DomainCommand.php: -------------------------------------------------------------------------------- 1 | line('Current application domain: '.$this->laravel['domain']. "--" . $this->laravel['domain_port'] . "--" . $this->laravel['domain_scheme'].''); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY=base64:BTJyFYeDfLauAbFziT1uwpskmFoLKk2eT2Kii5w3ksg= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | APP_URL=http://localhost 7 | 8 | DB_CONNECTION=mysql 9 | DB_HOST=127.0.0.1 10 | DB_PORT=3306 11 | DB_DATABASE=homestead 12 | DB_USERNAME=homestead 13 | DB_PASSWORD=secret 14 | 15 | BROADCAST_DRIVER=log 16 | CACHE_DRIVER=file 17 | SESSION_DRIVER=file 18 | SESSION_LIFETIME=120 19 | QUEUE_CONNECTION=database 20 | QUEUE_DEFAULT=default 21 | 22 | REDIS_HOST=127.0.0.1 23 | REDIS_PASSWORD=null 24 | REDIS_PORT=6379 25 | 26 | MAIL_DRIVER=smtp 27 | MAIL_HOST=smtp.mailtrap.io 28 | MAIL_PORT=2525 29 | MAIL_USERNAME=null 30 | MAIL_PASSWORD=null 31 | MAIL_ENCRYPTION=null 32 | 33 | PUSHER_APP_ID= 34 | PUSHER_APP_KEY= 35 | PUSHER_APP_SECRET= 36 | PUSHER_APP_CLUSTER=mt1 37 | -------------------------------------------------------------------------------- /src/Foundation/Configuration/ApplicationBuilder.php: -------------------------------------------------------------------------------- 1 | app->singleton( 17 | \Illuminate\Contracts\Http\Kernel::class, 18 | \Gecche\Multidomain\Foundation\Http\Kernel::class, 19 | ); 20 | 21 | $this->app->singleton( 22 | \Illuminate\Contracts\Console\Kernel::class, 23 | \Gecche\Multidomain\Foundation\Console\Kernel::class, 24 | ); 25 | 26 | return $this; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 31 | 32 | 33 |
34 |
35 |
{{env("APP_NAME")}}
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/app/Jobs/AppNameJob.php: -------------------------------------------------------------------------------- 1 | delete($filename); 25 | $files->append($filename,env('APP_NAME') . " --- " . $this->job->getQueue()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 25 | 26 | exit($status); 27 | -------------------------------------------------------------------------------- /tests/artisan_sub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 25 | 26 | exit($status); 27 | -------------------------------------------------------------------------------- /src/Horizon/SupervisorProcess.php: -------------------------------------------------------------------------------- 1 | push( 25 | MasterSupervisor::commandQueue(), 26 | AddSupervisor::class, 27 | $this->options->toArray() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/app/TestServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__ . '/../routes/web.php'); 29 | $this->loadMigrationsFrom( 30 | __DIR__ . '/../database/migrations' 31 | ); 32 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'multidomain'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Console/Application.php: -------------------------------------------------------------------------------- 1 | addOption($this->getDomainOption()); 18 | 19 | return $definition; 20 | } 21 | 22 | /** 23 | * Get the global environment option for the definition. 24 | * 25 | * @return \Symfony\Component\Console\Input\InputOption 26 | */ 27 | protected function getDomainOption() 28 | { 29 | $message = 'The domain the command should run under.'; 30 | 31 | return new InputOption('--domain', null, InputOption::VALUE_OPTIONAL, $message); 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/Foundation/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | supervisors[] = new SupervisorProcess( 29 | $options, $this->createProcess($master, $options), function ($type, $line) use ($master) { 30 | $master->output($type, $line); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 gecche 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Queue/QueueServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('queue.listener', function () { 23 | return new Listener($this->app->basePath()); 24 | }); 25 | } 26 | 27 | /** 28 | * Extends the queue listen command 29 | * 30 | * @return void 31 | */ 32 | public function register() 33 | { 34 | parent::register(); 35 | 36 | $this->app->extend(ListenCommand::class, function ($command, $app) { 37 | return new QueueListenCommand($app['queue.listener']); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/src/CommandsSubfolderTestCase.php: -------------------------------------------------------------------------------- 1 | laravelAppPath = __DIR__ . '/../../vendor/orchestra/testbench-core/laravel'; 25 | $this->laravelEnvPath = $this->laravelAppPath . DIRECTORY_SEPARATOR . 'envs'; 26 | } 27 | 28 | protected function resolveApplication() 29 | { 30 | return tap(new Application($_ENV['APP_BASE_PATH'],$_ENV['APP_BASE_PATH'].'/'.$this->envPath), function ($app) { 31 | $app->bind( 32 | 'Illuminate\Foundation\Bootstrap\LoadConfiguration', 33 | 'Orchestra\Testbench\Bootstrap\LoadConfiguration' 34 | ); 35 | }); 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /phpunit.xml.bak: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | src/ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests/src/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Queue/Console/ListenCommand.php: -------------------------------------------------------------------------------- 1 | hasOption('backoff')) ? $this->option('backoff') : $this->option('delay')); 23 | 24 | return new ListenerOptions( 25 | name: $this->option('name'), 26 | environment: $this->option('env'), 27 | backoff: $backoff, 28 | memory: $this->option('memory'), 29 | timeout: $this->option('timeout'), 30 | sleep: $this->option('sleep'), 31 | rest: $this->option('rest'), 32 | maxTries: $this->option('tries'), 33 | force: $this->option('force'), 34 | domain: $this->option('domain') 35 | ); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Horizon/HorizonApplicationServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(BaseSupervisorCommand::class, SupervisorCommand::class); 30 | $this->app->bind(BaseHorizonCommand::class, HorizonCommand::class); 31 | $this->app->bind(BaseTimeoutCommand::class, TimeoutCommand::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Queue/ListenerOptions.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 39 | 40 | parent::__construct($name, $environment, $backoff, $memory, $timeout, $sleep, $maxTries, $force, $rest); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire') 32 | // ->hourly(); 33 | } 34 | 35 | /** 36 | * Register the commands for the application. 37 | * 38 | * @return void 39 | */ 40 | protected function commands() 41 | { 42 | $this->load(__DIR__.'/Commands'); 43 | 44 | //require base_path('routes/console.php'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./tests/src/ 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./tests/src/ 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Horizon/QueueCommandString.php: -------------------------------------------------------------------------------- 1 | backoff, $options->maxTime, $options->maxJobs, $options->memory, 26 | $options->queue, $options->sleep, $options->timeout, $options->maxTries, 27 | (($options instanceof SupervisorOptions) ? $options->domain : 'localhost') 28 | ); 29 | 30 | if ($options->force) { 31 | $string .= ' --force'; 32 | } 33 | 34 | if ($paused) { 35 | $string .= ' --paused'; 36 | } 37 | 38 | return $string; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Horizon/Console/HorizonCommand.php: -------------------------------------------------------------------------------- 1 | find(MasterSupervisor::name())) { 26 | return $this->comment('A master supervisor is already running on this machine.'); 27 | } 28 | 29 | $master = (new MasterSupervisor)->handleOutputUsing(function ($type, $line) { 30 | $this->output->write($line); 31 | }); 32 | 33 | ProvisioningPlan::get(MasterSupervisor::name(), $this->option('domain'))->deploy( 34 | $this->option('environment') ?? config('horizon.env') ?? config('app.env') 35 | ); 36 | 37 | $this->info('Horizon started successfully.'); 38 | 39 | pcntl_async_signals(true); 40 | 41 | pcntl_signal(SIGINT, function () use ($master) { 42 | $this->line('Shutting down...'); 43 | 44 | return $master->terminate(); 45 | }); 46 | 47 | $master->monitor(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Foundation/helpers.php: -------------------------------------------------------------------------------- 1 | environmentPath().($path ? DIRECTORY_SEPARATOR.$path : $path); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/Horizon/Console/SupervisorCommand.php: -------------------------------------------------------------------------------- 1 | hasOption('backoff') 23 | ? $this->option('backoff') 24 | : $this->option('delay'); 25 | 26 | return new SupervisorOptions( 27 | $this->argument('name'), 28 | $this->argument('connection'), 29 | $this->getQueue($this->argument('connection')), 30 | $this->option('workers-name'), 31 | $this->option('balance'), 32 | $backoff, 33 | $this->option('max-time'), 34 | $this->option('max-jobs'), 35 | $this->option('max-processes'), 36 | $this->option('min-processes'), 37 | $this->option('memory'), 38 | $this->option('timeout'), 39 | $this->option('sleep'), 40 | $this->option('tries'), 41 | $this->option('force'), 42 | $this->option('nice'), 43 | $this->option('balance-cooldown'), 44 | $this->option('balance-max-shift'), 45 | $this->option('parent-id'), 46 | $this->option('domain') 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gecche/laravel-multidomain", 3 | "description": "Laravel App on a subdomains, multi-tenancy setting", 4 | "keywords": ["laravel", "subdomains", "multitenancy", "multi-tenants", "multidomain"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Giacomo Terreni", 9 | "email": "giacomo.terreni@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "laravel/framework": "^12.0" 14 | }, 15 | "require-dev": { 16 | "mockery/mockery": "^1.6", 17 | "phpunit/phpunit": "^11.5", 18 | "orchestra/testbench": "^10.0", 19 | "orchestra/testbench-browser-kit": "^10.0", 20 | "codedungeon/phpunit-result-printer": "^0.1.0" 21 | }, 22 | "autoload": { 23 | "classmap": [ 24 | 25 | ], 26 | "psr-4": { 27 | "Gecche\\Multidomain\\": "src/" 28 | }, 29 | "files": [ 30 | "src/Foundation/helpers.php" 31 | ] 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Gecche\\Multidomain\\Tests\\": "tests/src", 36 | "Gecche\\Multidomain\\Tests\\App\\": "tests/app" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "phpunit" 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "Gecche\\Multidomain\\Foundation\\Providers\\DomainConsoleServiceProvider" 46 | ] 47 | } 48 | }, 49 | "config": { 50 | "allow-plugins": { 51 | "kylekatarnls/update-helper": true 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable" : true 56 | } 57 | -------------------------------------------------------------------------------- /tests/database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainCommand":0.025,"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainAddCommand":0.019,"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainRemoveCommand":0.032,"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainRemoveForceCommand":0.026,"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainRemoveForceCommandSubsite":0.06,"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainUpdateEnvCommand":0.017,"Gecche\\Multidomain\\Tests\\CommandsSubfolderTestCase::testDomainListCommand":0.027,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainCommand":0.039,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainAddCommand":0.026,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainRemoveCommand":0.021,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainRemoveForceCommand":0.033,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainRemoveForceCommandSubsite":0.061,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainUpdateEnvCommand":0.026,"Gecche\\Multidomain\\Tests\\CommandsTestCase::testDomainListCommand":0.026,"Gecche\\Multidomain\\Tests\\HttpSubfolderTestCase::testWelcomePage":0.122,"Gecche\\Multidomain\\Tests\\HttpSubfolderTestCase::testDBConnection":0.108,"Gecche\\Multidomain\\Tests\\HttpSubfolderTestCase::testEnvFile":0.086,"Gecche\\Multidomain\\Tests\\HttpSubfolderTestCase::testStorageFolder":0.09,"Gecche\\Multidomain\\Tests\\HttpTestCase::testWelcomePage":0.149,"Gecche\\Multidomain\\Tests\\HttpTestCase::testDBConnection":0.132,"Gecche\\Multidomain\\Tests\\HttpTestCase::testEnvFile":0.093,"Gecche\\Multidomain\\Tests\\HttpTestCase::testStorageFolder":0.122}} -------------------------------------------------------------------------------- /tests/bootstrap/app.php: -------------------------------------------------------------------------------- 1 | $hasEnvironmentFile, 'extra' => $config->getExtraAttributes()], 28 | resolvingCallback: static function ($app) use ($config) { 29 | Workbench::startWithProviders($app, $config); 30 | Workbench::discoverRoutes($app, $config); 31 | }, 32 | ); 33 | }; 34 | 35 | if (! defined('TESTBENCH_WORKING_PATH') && ! is_null(Env::get('TESTBENCH_WORKING_PATH'))) { 36 | define('TESTBENCH_WORKING_PATH', Env::get('TESTBENCH_WORKING_PATH')); 37 | } 38 | 39 | $app = $createApp(realpath(join_paths(__DIR__, '..'))); 40 | 41 | unset($createApp); 42 | 43 | /** @var \Illuminate\Routing\Router $router */ 44 | $router = $app->make('router'); 45 | 46 | collect(glob(join_paths(__DIR__, '..', 'routes', 'testbench-*.php'))) 47 | ->each(static function ($routeFile) use ($app, $router) { 48 | require $routeFile; 49 | }); 50 | 51 | return $app; 52 | -------------------------------------------------------------------------------- /src/Queue/Listener.php: -------------------------------------------------------------------------------- 1 | createCommand( 26 | $connection, 27 | $queue, 28 | $options 29 | ); 30 | 31 | if ($options instanceof ListenerOptions && $options->domain !== null) { 32 | $command = $this->addDomain($command, $options); 33 | } 34 | 35 | // If the environment is set, we will append it to the command string so the 36 | // workers will run under the specified environment. Otherwise, they will 37 | // just run under the production environment which is not always right. 38 | if (isset($options->environment)) { 39 | $command = $this->addEnvironment($command, $options); 40 | } 41 | 42 | return new Process( 43 | $command, $this->commandPath, null, null, $options->timeout 44 | ); 45 | } 46 | 47 | /** 48 | * Add the domain option to the given command. 49 | * 50 | * @param array $command 51 | * @param ListenerOptions $options 52 | * @return array 53 | */ 54 | protected function addDomain(array $command, ListenerOptions $options) : array 55 | { 56 | return array_merge($command, ["--domain={$options->domain}"]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/Horizon/ProvisioningPlan.php: -------------------------------------------------------------------------------- 1 | $domain] + $supervisor : $supervisor); 31 | }, $group) : $group); 32 | }, $environments) : $environments); 33 | 34 | return new static($master, $environments, config('horizon.defaults', [])); 35 | } 36 | 37 | /** 38 | * Add a supervisor with the given options. 39 | * 40 | * @param BaseSupervisorOptions $options 41 | * @return void 42 | */ 43 | protected function add(BaseSupervisorOptions $options) 44 | { 45 | app(HorizonCommandQueue::class)->push( 46 | MasterSupervisor::commandQueueFor($this->master), 47 | AddSupervisor::class, 48 | $options->toArray() 49 | ); 50 | } 51 | 52 | /** 53 | * Convert the given array of options into a SupervisorOptions instance. 54 | * 55 | * @param string $supervisor 56 | * @param array $options 57 | * @return SupervisorOptions 58 | */ 59 | protected function convert($supervisor, $options) 60 | { 61 | return SupervisorOptions::fromArray(['domain' => $options['domain']] + parent::convert($supervisor, $options)->toArray()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | 2 | # laravel-multidomain (tests' notes) 3 | We provide some information about laravel-multidomain's tests. 4 | 5 | ## Organization 6 | We provide 3 test suites: 7 | 8 | 1. `CommandsTestCase` where we include a test for each of the commands 9 | provided by the `DomainConsoleServiceProvider` provider. 10 | 11 | 2. `HttpTestCase` where we include some tests for simulating HTTP requests 12 | changing the domain name. We provide two distinct settings (other than the default one) 13 | for two additional domains, namely `site1.test` and `site2.test`. 14 | 15 | 3. `ArtisanTestCase` where we include some tests for simulating the `--domain` 16 | option applied to Artisan commands. 17 | 18 | ## Requirements and setup 19 | We make use of the fantastic [Orchestra/Testbench](https://github.com/orchestral/testbench) 20 | package together with its browser-kit extension. 21 | 22 | However, in order to setup the Laravel application with the machinery provided 23 | by our package, we provide a standard `.env.example` file and some configuration files 24 | to be used by the simulated Orchestra's Laravel application. 25 | 26 | Moreover we created an adapted copy of the Laravel's `artisan` script in order to 27 | use the `--domain` option in the Artisan commands launched by shell. 28 | 29 | Note that, as pointed out in the package documentation, the `--domain` option does 30 | not work from within a Laravel application, so we needed to simulate command launches 31 | directly from shell. 32 | 33 | We perform above operations in the `setUp` sections of the suites. 34 | 35 | We provide tests under the `mysql` connection (set in `phpunit.xml`). 36 | You need to create two databases, namely (`site1` and `site2`) in your DB server 37 | in order to run tests correctly. 38 | 39 | ## Running tests 40 | As usual, we run tests from the package folder launching: 41 | 42 | ```bash 43 | ../../../vendor/bin/phpunit 44 | ``` 45 | 46 | However, as we need to simulate more than one domain via `$_SERVER['SERVER_NAME']` values 47 | and as in the `phpunit.xml` file we can't put a conditional environmentt variable, 48 | in order to fullyperform all the tests, we require to launch also the following 49 | commands: 50 | 51 | ```bash 52 | SERVER_NAME=site1.test ../../../vendor/bin/phpunit 53 | ``` 54 | 55 | ```bash 56 | SERVER_NAME=site2.test ../../../vendor/bin/phpunit 57 | ``` 58 | -------------------------------------------------------------------------------- /tests/app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 36 | EncryptCookies::class, 37 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 38 | \Illuminate\Session\Middleware\StartSession::class, 39 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 40 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 41 | VerifyCsrfToken::class, 42 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 43 | ], 44 | 45 | 'api' => [ 46 | 'throttle:60,1', 47 | 'bindings', 48 | ], 49 | ]; 50 | 51 | /** 52 | * The application's route middleware. 53 | * 54 | * These middleware may be assigned to groups or used individually. 55 | * 56 | * @var array 57 | */ 58 | protected $routeMiddleware = [ 59 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 60 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 61 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 62 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 63 | 'guest' => RedirectIfAuthenticated::class, 64 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /src/Foundation/Console/RemoveDomainCommand.php: -------------------------------------------------------------------------------- 1 | domain = $this->argument('domain'); 28 | 29 | /* 30 | * CREATE ENV FILE FOR DOMAIN 31 | */ 32 | $this->deleteDomainEnvFile(); 33 | 34 | 35 | /* 36 | * Setting domain storage directories 37 | */ 38 | 39 | if ($this->option('force')) { 40 | $this->deleteDomainStorageDirs(); 41 | } 42 | 43 | /* 44 | * Update config file 45 | */ 46 | 47 | $this->updateConfigFile('remove'); 48 | 49 | $this->line("Removed " . $this->domain . " from the application."); 50 | } 51 | 52 | 53 | /** 54 | * Get the stub file for the generator. 55 | * 56 | * @return string 57 | */ 58 | protected function getStub() 59 | { 60 | 61 | } 62 | 63 | protected function deleteDomainEnvFile() 64 | { 65 | $domainEnvFilePath = $this->getDomainEnvFilePath(); 66 | if ($this->files->exists($domainEnvFilePath)) { 67 | $this->files->delete($domainEnvFilePath); 68 | } 69 | } 70 | 71 | public function deleteDomainStorageDirs() { 72 | $path = $this->getDomainStoragePath($this->domain); 73 | if ($this->files->exists($path)) { 74 | $this->files->deleteDirectory($path); 75 | } 76 | } 77 | 78 | protected function removeDomainToConfigFile($config) { 79 | $domains = Arr::get($config, 'domains', []); 80 | if (array_key_exists($this->domain, $domains)) { 81 | unset($domains[$this->domain]); 82 | } 83 | $config['domains'] = $domains; 84 | return $config; 85 | } 86 | 87 | 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Foundation/Console/ListDomainCommand.php: -------------------------------------------------------------------------------- 1 | configFile . '.php'); 32 | 33 | $config = include $filename; 34 | 35 | /* 36 | * GET DOMAINS BASED ON domains KEY IN THE CONFIG FILE 37 | */ 38 | $domains = Arr::get($config, 'domains', []); 39 | 40 | 41 | /* 42 | * Simply returns the info for each domain found in config. 43 | */ 44 | $outputType = $this->option('output'); 45 | $domains = $this->buildResult($domains); 46 | switch (strtolower(trim($outputType ?? 'txt'))) { 47 | default: 48 | case 'txt': 49 | $this->outputAsText($domains); 50 | break; 51 | case 'table': 52 | $this->outputAsTable($domains); 53 | break; 54 | case 'json': 55 | $this->outputAsJson($domains); 56 | break; 57 | } 58 | } 59 | 60 | protected function outputAsText(array $domains) 61 | { 62 | foreach ($domains as $domain) { 63 | $this->line("Domain: " . Arr::get($domain,'domain') . ""); 64 | 65 | $this->line(" - Storage dir: " . Arr::get($domain,'storage_dir') . ""); 66 | $this->line(" - Env file: " . Arr::get($domain,'env_file') . ""); 67 | 68 | $this->line(""); 69 | 70 | } 71 | } 72 | 73 | protected function outputAsJson(array $domains) 74 | { 75 | $this->output->writeln(json_encode($domains)); 76 | } 77 | 78 | protected function outputAsTable(array $domains) 79 | { 80 | $this->output->table(array_keys(head($domains)), $domains); 81 | } 82 | 83 | protected function buildResult(array $domains): array 84 | { 85 | $result = []; 86 | foreach ($domains as $domain) { 87 | $result [] = [ 88 | 'domain' => $domain, 89 | 'storage_dir' => $this->getDomainStoragePath($domain), 90 | 'env_file' => $this->getDomainEnvFilePath($domain), 91 | ]; 92 | } 93 | 94 | return $result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => env('QUEUE_DEFAULT'), 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => 'default', 48 | 'retry_after' => 90, 49 | 'block_for' => 0, 50 | ], 51 | 52 | 'sqs' => [ 53 | 'driver' => 'sqs', 54 | 'key' => env('AWS_ACCESS_KEY_ID'), 55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 58 | 'suffix' => env('SQS_SUFFIX'), 59 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 60 | ], 61 | 62 | 'redis' => [ 63 | 'driver' => 'redis', 64 | 'connection' => 'default', 65 | 'queue' => env('REDIS_QUEUE', 'default'), 66 | 'retry_after' => 90, 67 | 'block_for' => null, 68 | ], 69 | 70 | ], 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Failed Queue Jobs 75 | |-------------------------------------------------------------------------- 76 | | 77 | | These options configure the behavior of failed queue job logging so you 78 | | can control which database and table are used to store the jobs that 79 | | have failed. You may change them to any database / table you wish. 80 | | 81 | */ 82 | 83 | 'failed' => [ 84 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 85 | 'database' => env('DB_CONNECTION', 'mysql'), 86 | 'table' => 'failed_jobs', 87 | ], 88 | 89 | ]; 90 | -------------------------------------------------------------------------------- /src/Foundation/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | artisan)) 36 | { 37 | $this->artisan = (new Artisan($this->app, $this->events, $this->app->version())) 38 | ->resolveCommands($this->commands) 39 | ->setContainerCommandLoader(); 40 | 41 | if ($this->symfonyDispatcher instanceof EventDispatcher) { 42 | $this->artisan->setDispatcher($this->symfonyDispatcher); 43 | } 44 | } 45 | 46 | return $this->artisan; 47 | } 48 | 49 | 50 | /** 51 | * Run an Artisan console command by name. 52 | * 53 | * @param string $command 54 | * @param array $parameters 55 | * @param \Symfony\Component\Console\Output\OutputInterface $outputBuffer 56 | * @return int 57 | */ 58 | public function call($command, array $parameters = [], $outputBuffer = null, $forceBootstrap = false) 59 | { 60 | if ($forceBootstrap) { 61 | $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : []; 62 | $argvDomain = Arr::first($argv, function ($value) { 63 | return Str::startsWith($value, '--domain'); 64 | }); 65 | if (!$argvDomain) { 66 | $paramDomain = Arr::get($parameters,'--domain'); 67 | if ($paramDomain) { 68 | $_SERVER['argv'][] = $paramDomain; 69 | } 70 | } 71 | $this->app->bootstrapWith($this->bootstrappers()); 72 | } 73 | 74 | $this->bootstrap(); 75 | 76 | return $this->getArtisan()->call($command, $parameters, $outputBuffer); 77 | } 78 | 79 | protected function shouldDiscoverCommands() 80 | { 81 | if (get_class($this) === __CLASS__) { 82 | return true; 83 | } 84 | return parent::shouldDiscoverCommands(); // TODO: Change the autogenerated stub 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Foundation/Console/UpdateEnvDomainCommand.php: -------------------------------------------------------------------------------- 1 | domain = $this->argument('domain'); 33 | 34 | $this->envFiles = $this->getDomainEnvFiles(); 35 | 36 | /* 37 | * CREATE ENV FILE FOR DOMAIN 38 | */ 39 | $this->updateDomainEnvFiles(); 40 | 41 | $this->line("Updated env domain files"); 42 | } 43 | 44 | 45 | protected function getDomainEnvFiles() 46 | { 47 | 48 | if ($this->domain) { 49 | if ($this->files->exists($this->getDomainEnvFilePath())) { 50 | return [$this->getDomainEnvFilePath()]; 51 | } 52 | } 53 | 54 | $envFiles = [ 55 | env_path('.env'), 56 | ]; 57 | $domainList = Config::get('domain.domains',[]); 58 | 59 | 60 | foreach ($domainList as $domain) { 61 | $domainFile = $this->getDomainEnvFilePath($domain); 62 | if ($this->files->exists($domainFile)) { 63 | $envFiles[] = $domainFile; 64 | } 65 | } 66 | 67 | return $envFiles; 68 | 69 | } 70 | 71 | 72 | protected function updateDomainEnvFiles() 73 | { 74 | $domainValues = json_decode($this->option("domain_values"), true); 75 | // $this->line("".var_dump($this->option("domain_values")).""); 76 | // $this->line("".var_dump($domainValues).""); 77 | 78 | if (!is_array($domainValues)) { 79 | $domainValues = array(); 80 | } 81 | 82 | 83 | 84 | 85 | 86 | 87 | foreach ($this->envFiles as $envFilePath) { 88 | $envArray = $this->getVarsArray($envFilePath); 89 | 90 | $domainEnvArray = array_merge($envArray, $domainValues); 91 | $domainEnvFileContents = $this->makeDomainEnvFileContents($domainEnvArray); 92 | 93 | $this->files->put($envFilePath, $domainEnvFileContents); 94 | } 95 | } 96 | 97 | /** 98 | * Get the stub file for the generator. 99 | * 100 | * @return string 101 | */ 102 | protected function getStub() 103 | { 104 | 105 | } 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Foundation/Providers/DomainConsoleServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->alias('artisan',\Gecche\Multidomain\Console\Application::class); 38 | 39 | 40 | foreach ($this->commands as $command) 41 | { 42 | $this->{"register{$command}Command"}(); 43 | } 44 | 45 | $this->commands( 46 | "command.domain", 47 | "command.domain.add", 48 | "command.domain.remove", 49 | "command.domain.update_env", 50 | "command.domain.list" 51 | ); 52 | 53 | } 54 | 55 | 56 | public function boot() { 57 | $this->publishes([ 58 | __DIR__.'/../../config/domain.php' => config_path('domain.php'), 59 | ]); 60 | } 61 | 62 | 63 | /** 64 | * Register the command. 65 | * 66 | * @return void 67 | */ 68 | protected function registerDomainCommand() 69 | { 70 | $this->app->singleton('command.domain', function() 71 | { 72 | return new DomainCommand; 73 | }); 74 | } 75 | 76 | /** 77 | * Register the command. 78 | * 79 | * @return void 80 | */ 81 | protected function registerAddDomainCommand() 82 | { 83 | $this->app->singleton('command.domain.add', function($app) 84 | { 85 | return new AddDomainCommand($app['files']); 86 | }); 87 | } 88 | 89 | /** 90 | * Register the command. 91 | * 92 | * @return void 93 | */ 94 | protected function registerRemoveDomainCommand() 95 | { 96 | $this->app->singleton('command.domain.remove', function($app) 97 | { 98 | return new RemoveDomainCommand($app['files']); 99 | }); 100 | } 101 | 102 | /** 103 | * Register the command. 104 | * 105 | * @return void 106 | */ 107 | protected function registerUpdateEnvDomainCommand() 108 | { 109 | $this->app->singleton('command.domain.update_env', function($app) 110 | { 111 | return new UpdateEnvDomainCommand($app['files']); 112 | }); 113 | } 114 | 115 | /** 116 | * Register the command. 117 | * 118 | * @return void 119 | */ 120 | protected function registerListDomainCommand() 121 | { 122 | $this->app->singleton('command.domain.list', function($app) 123 | { 124 | return new ListDomainCommand($app['files']); 125 | }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Foundation/DomainDetector.php: -------------------------------------------------------------------------------- 1 | domainDetectionFunctionWeb = $domainDetectionFunctionWeb; 24 | 25 | } 26 | 27 | 28 | /** 29 | * Detect the application's current environment. 30 | * 31 | * @param array|string $environments 32 | * @param array|null $consoleArgs 33 | * @return string 34 | */ 35 | public function detect($consoleArgs = null) 36 | { 37 | if ($consoleArgs) 38 | { 39 | return $this->detectConsoleDomain($consoleArgs); 40 | } 41 | else 42 | { 43 | return $this->detectWebDomain(); 44 | } 45 | } 46 | 47 | /** 48 | * Set the application environment for a web request. 49 | * 50 | * @param array|string $environments 51 | * @return string 52 | */ 53 | protected function detectWebDomain() 54 | { 55 | if ($this->domainDetectionFunctionWeb instanceof Closure) { 56 | return ($this->domainDetectionFunctionWeb)(); 57 | } 58 | //return filter_input(INPUT_SERVER,'SERVER_NAME'); 59 | return Arr::get($_SERVER,'SERVER_NAME'); 60 | 61 | } 62 | 63 | /** 64 | * Set the application environment from command-line arguments. 65 | * 66 | * @param mixed $environments 67 | * @param array $args 68 | * @return string 69 | */ 70 | protected function detectConsoleDomain(array $args) 71 | { 72 | // First we will check if an environment argument was passed via console arguments 73 | // and if it was that automatically overrides as the environment. Otherwise, we 74 | // will check the environment as a "web" request like a typical HTTP request. 75 | if ( ! is_null($value = $this->getDomainArgument($args))) 76 | { 77 | return head(array_slice(explode('=', $value), 1)); 78 | } 79 | else 80 | { 81 | return $this->detectWebDomain(); 82 | } 83 | } 84 | 85 | /** 86 | * Get the environment argument from the console. 87 | * 88 | * @param array $args 89 | * @return string|null 90 | */ 91 | protected function getDomainArgument(array $args) 92 | { 93 | return Arr::first($args, function ($value) { 94 | return Str::startsWith($value, '--domain'); 95 | }); 96 | } 97 | 98 | /* 99 | * Split the domain name into scheme, name and port 100 | */ 101 | public function split($domain) { 102 | $domain = $domain ?? ''; 103 | 104 | if (Str::startsWith($domain,'https://')) { 105 | $scheme = 'https'; 106 | $domain = substr($domain,8); 107 | } elseif (Str::startsWith($domain,'http://')) { 108 | $scheme = 'http'; 109 | $domain = substr($domain,7); 110 | } else { 111 | $scheme = 'http'; 112 | } 113 | 114 | $semicolon = strpos($domain,':'); 115 | if ($semicolon === false) { 116 | $port = ($scheme == 'http') ? 80 : 443; 117 | } else { 118 | $port = substr($domain,$semicolon+1); 119 | $domain = substr($domain,0,-(strlen($port)+1)); 120 | } 121 | 122 | return array($scheme,$domain,$port); 123 | 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Horizon/SupervisorOptions.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 66 | } 67 | 68 | /** 69 | * Get the command-line representation of the options for a supervisor. 70 | * 71 | * @return string 72 | */ 73 | public function toSupervisorCommand() 74 | { 75 | return SupervisorCommandString::fromOptions($this); 76 | } 77 | 78 | /** 79 | * Get the command-line representation of the options for a worker. 80 | * 81 | * @return string 82 | */ 83 | public function toWorkerCommand() 84 | { 85 | return WorkerCommandString::fromOptions($this); 86 | } 87 | 88 | /** 89 | * Convert the options to a raw array. 90 | * 91 | * @return array 92 | */ 93 | public function toArray() 94 | { 95 | return [ 96 | 'balance' => $this->balance, 97 | 'connection' => $this->connection, 98 | 'queue' => $this->queue, 99 | 'backoff' => $this->backoff, 100 | 'force' => $this->force, 101 | 'maxProcesses' => $this->maxProcesses, 102 | 'minProcesses' => $this->minProcesses, 103 | 'maxTries' => $this->maxTries, 104 | 'maxTime' => $this->maxTime, 105 | 'maxJobs' => $this->maxJobs, 106 | 'memory' => $this->memory, 107 | 'nice' => $this->nice, 108 | 'name' => $this->name, 109 | 'workersName' => $this->workersName, 110 | 'sleep' => $this->sleep, 111 | 'timeout' => $this->timeout, 112 | 'balanceCooldown' => $this->balanceCooldown, 113 | 'balanceMaxShift' => $this->balanceMaxShift, 114 | 'domain' => $this->domain 115 | ]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Foundation/Console/DomainCommandTrait.php: -------------------------------------------------------------------------------- 1 | domain; 26 | } 27 | 28 | return rtrim(env_path('.env.' . $domain),'.'); 29 | } 30 | 31 | /** 32 | * Returns the path of the storage folder for the specified domain 33 | * @param null $domain 34 | * @return string 35 | */ 36 | protected function getDomainStoragePath($domain = null) 37 | { 38 | $path = app()->exactDomainStoragePath($domain); 39 | return $path; 40 | } 41 | 42 | /** 43 | * Returns the contents of the stub of the package's configuration file 44 | * @return mixed 45 | */ 46 | protected function getConfigStub() 47 | { 48 | $filename = base_path('stubs/domain/config.stub'); 49 | 50 | if (!$this->files->exists($filename)) { 51 | $filename = __DIR__ . '/../../stubs/config.stub'; 52 | } 53 | 54 | return $this->files->get($filename); 55 | } 56 | 57 | /** 58 | * This method updates the package's config file by adding or removing the domain handled by the caller command. 59 | * It calls either the addDomainToConfigFile or the removeDomainToConfigFile method of the caller. 60 | * 61 | * @param string $opType (add|remove) 62 | */ 63 | protected function updateConfigFile($opType = 'add') 64 | { 65 | $filename = base_path('config/' . $this->configFile . '.php'); 66 | 67 | $config = include $filename; 68 | 69 | $configStub = $this->getConfigStub(); 70 | 71 | $methodName = $opType . 'DomainToConfigFile'; 72 | 73 | $finalConfig = call_user_func_array([$this, $methodName], [$config]); 74 | 75 | $modelConfigStub = str_replace( 76 | '{{$configArray}}', var_export($finalConfig, true), $configStub 77 | ); 78 | 79 | $modelConfigStub = str_replace( 80 | 'return array (', 'return [', $modelConfigStub 81 | ); 82 | $modelConfigStub = str_replace( 83 | ');', ' ];', $modelConfigStub 84 | ); 85 | $modelConfigStub = str_replace( 86 | ["\narray (", "\n array (", "\n array (", "\n array ("], '[', $modelConfigStub 87 | ); 88 | $modelConfigStub = str_replace( 89 | ["),"], "],", $modelConfigStub 90 | ); 91 | 92 | $this->files->put($filename, $modelConfigStub); 93 | Config::set($this->configFile, $finalConfig); 94 | } 95 | 96 | /** 97 | * This method gets the contents of a file formatted as a standard .env file 98 | * i.e. with each line in the form of KEY=VALUE 99 | * and returns the entries as an array 100 | * 101 | * @param $path 102 | * @return array 103 | */ 104 | protected function getVarsArray($path) 105 | { 106 | $envFileContents = $this->files->get($path); 107 | $envFileContentsArray = explode("\n", $envFileContents); 108 | $varsArray = array(); 109 | foreach ($envFileContentsArray as $line) { 110 | $lineArray = explode('=', $line); 111 | 112 | //Skip the line if there is no '=' 113 | if (count($lineArray) < 2) { 114 | continue; 115 | } 116 | 117 | $value = substr($line, strlen($lineArray[0])+1); 118 | $varsArray[$lineArray[0]] = trim($value); 119 | 120 | } 121 | return $varsArray; 122 | } 123 | 124 | /** 125 | * This method prepares the values of an .env file to be stored 126 | * @param $domainValues 127 | * @return string 128 | */ 129 | protected function makeDomainEnvFileContents($domainValues) 130 | { 131 | $contents = ''; 132 | $previousKeyPrefix = ''; 133 | foreach ($domainValues as $key => $value) { 134 | $keyPrefix = current(explode('_', $key)); 135 | if ($keyPrefix !== $previousKeyPrefix && !empty($contents)) { 136 | $contents .= "\n"; 137 | } 138 | $contents .= $key . '=' . $value . "\n"; 139 | $previousKeyPrefix = $keyPrefix; 140 | } 141 | return $contents; 142 | } 143 | 144 | 145 | } 146 | -------------------------------------------------------------------------------- /tests/src/HttpSubfolderTestCase.php: -------------------------------------------------------------------------------- 1 | laravelAppPath = __DIR__ . '/../../vendor/orchestra/testbench-core/laravel'; 70 | copy(__DIR__ . '/../artisan_sub',$this->laravelAppPath.'/artisan_sub'); 71 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'config:clear']); 72 | $process->run(); 73 | 74 | 75 | $this->files = new Filesystem(); 76 | copy($this->laravelAppPath . '/config/app.php', $this->laravelAppPath . '/config/appORIG.php'); 77 | copy(__DIR__ . '/../config/app.php', $this->laravelAppPath . '/config/app.php'); 78 | if (!is_dir($this->laravelAppPath.'/'.$this->envPath)) { 79 | mkdir($this->laravelAppPath.'/'.$this->envPath); 80 | } 81 | copy(__DIR__ . '/../.env.example', $this->laravelAppPath.'/'.$this->envPath.'/.env'); 82 | 83 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'vendor:publish', '--provider="Gecche\Multidomain\Foundation\Providers\DomainConsoleServiceProvider"']); 84 | $process->run(); 85 | 86 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:remove', $this->site1, '--force']); 87 | $process->run(); 88 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:remove', $this->site2, '--force']); 89 | $process->run(); 90 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:remove', $this->subSite1, '--force']); 91 | $process->run(); 92 | 93 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:add', $this->site1]); 94 | $process->run(); 95 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:add', $this->site2]); 96 | $process->run(); 97 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:add', $this->subSite1]); 98 | $process->run(); 99 | 100 | $domainValues = [ 101 | 'APP_NAME' => $this->siteAppName1, 102 | 'DB_DATABASE' => $this->siteDbName1, 103 | ]; 104 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:update_env', $this->site1, '--domain_values='.json_encode($domainValues)]); 105 | $process->run(); 106 | 107 | $domainValues = [ 108 | 'APP_NAME' => $this->siteAppName2, 109 | 'DB_DATABASE' => $this->siteDbName2, 110 | ]; 111 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:update_env', $this->site2, '--domain_values='.json_encode($domainValues)]); 112 | $process->run(); 113 | 114 | $domainValues = [ 115 | 'APP_NAME' => $this->subSiteAppName1, 116 | 'DB_DATABASE' => $this->subSiteDbName1, 117 | ]; 118 | $process = new Process(['php', $this->laravelAppPath.'/artisan_sub', 'domain:update_env', $this->subSite1, '--domain_values='.json_encode($domainValues)]); 119 | $process->run(); 120 | 121 | TestCase::setUp(); 122 | 123 | 124 | } 125 | 126 | 127 | protected function resolveApplication() 128 | { 129 | return tap(new Application($_ENV['APP_BASE_PATH'], $_ENV['APP_BASE_PATH'].'/'.$this->envPath), function ($app) { 130 | $app->bind( 131 | 'Illuminate\Foundation\Bootstrap\LoadConfiguration', 132 | 'Orchestra\Testbench\Bootstrap\LoadConfiguration' 133 | ); 134 | }); 135 | } 136 | 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/Foundation/Console/AddDomainCommand.php: -------------------------------------------------------------------------------- 1 | domain = $this->argument('domain'); 34 | 35 | /* 36 | * CREATE ENV FILE FOR DOMAIN 37 | */ 38 | $this->createDomainEnvFile(); 39 | 40 | 41 | /* 42 | * Setting domain storage directories 43 | */ 44 | 45 | $this->createDomainStorageDirs(); 46 | 47 | /* 48 | * Update config file 49 | */ 50 | $this->updateConfigFile(); 51 | 52 | /** 53 | * Configuring .gitignore file according local requirements 54 | */ 55 | if ($this->option('dev')) { 56 | $this->setupGitIgnore(); 57 | } 58 | 59 | $this->line("Added " . $this->domain . " to the application." . ($this->option('dev') ? ' (In dev mode)' : '')); 60 | } 61 | 62 | 63 | /** 64 | * Get the stub file for the generator. 65 | * 66 | * @return string 67 | */ 68 | protected function getStub() 69 | { 70 | if ($this->files->exists($this->getDomainEnvFilePath())) { 71 | return $this->getDomainEnvFilePath(); 72 | } 73 | return env_path(Config::get('domain.env_stub', '.env')); 74 | } 75 | 76 | protected function createDomainEnvFile() 77 | { 78 | $envFilePath = $this->getStub(); 79 | 80 | $domainValues = json_decode($this->option("domain_values"), true); 81 | 82 | 83 | if (!is_array($domainValues)) { 84 | $domainValues = array(); 85 | } 86 | 87 | 88 | $envArray = $this->getVarsArray($envFilePath); 89 | 90 | $domainEnvFilePath = $this->getDomainEnvFilePath(); 91 | 92 | $domainEnvArray = array_merge($envArray, $domainValues); 93 | $domainEnvFileContents = $this->makeDomainEnvFileContents($domainEnvArray); 94 | 95 | $this->files->put($domainEnvFilePath, $domainEnvFileContents); 96 | } 97 | 98 | public function createDomainStorageDirs() 99 | { 100 | $storageDirs = Config::get('domain.storage_dirs', array()); 101 | $path = $this->getDomainStoragePath($this->domain); 102 | $rootPath = storage_path(); 103 | if ($this->files->exists($path) && !$this->option('force')) { 104 | return; 105 | } 106 | 107 | if ($this->files->exists($path)) { 108 | $this->files->deleteDirectory($path); 109 | } 110 | 111 | 112 | $this->createRecursiveDomainStorageDirs($rootPath, $path, $storageDirs); 113 | 114 | 115 | } 116 | 117 | protected function createRecursiveDomainStorageDirs($rootPath, $path, $dirs) 118 | { 119 | $this->files->makeDirectory($path, 0755, true); 120 | foreach (['.gitignore', '.gitkeep'] as $gitFile) { 121 | $rootGitPath = $rootPath . DIRECTORY_SEPARATOR . $gitFile; 122 | if ($this->files->exists($rootGitPath)) { 123 | $gitPath = $path . DIRECTORY_SEPARATOR . $gitFile; 124 | $this->files->copy($rootGitPath, $gitPath); 125 | } 126 | } 127 | foreach ($dirs as $subdir => $subsubdirs) { 128 | $fullPath = $path . DIRECTORY_SEPARATOR . $subdir; 129 | $fullRootPath = $rootPath . DIRECTORY_SEPARATOR . $subdir; 130 | $this->createRecursiveDomainStorageDirs($fullRootPath, $fullPath, $subsubdirs); 131 | } 132 | 133 | } 134 | 135 | protected function addDomainToConfigFile($config) { 136 | $domains = Arr::get($config, 'domains', []); 137 | if (!array_key_exists($this->domain, $domains)) { 138 | $domains[$this->domain] = $this->domain; 139 | } 140 | 141 | ksort($domains); 142 | $config['domains'] = $domains; 143 | 144 | return $config; 145 | } 146 | 147 | protected function setupGitIgnore() 148 | { 149 | $toplevelDomain = array_reverse(explode('.', $this->domain))[0]; 150 | 151 | $finds = [ 152 | 'domains' => Config::get('domain.env_stub', '.env') . ".*.{$toplevelDomain}", 153 | 'storage' => DIRECTORY_SEPARATOR . "storage" . DIRECTORY_SEPARATOR . "*_{$toplevelDomain}", 154 | ]; 155 | 156 | foreach(file(base_path('.gitignore'), FILE_SKIP_EMPTY_LINES) as $line) { 157 | foreach ($finds as $key => $value) { 158 | if (strpos($line, $value) === 0) { 159 | $finds[$key] = null; 160 | } 161 | } 162 | } 163 | 164 | $contents = implode("\n", $finds); 165 | 166 | if (empty(trim($contents))) { 167 | return; 168 | } 169 | 170 | $fh = fopen(base_path('.gitignore'), 'a'); 171 | 172 | fwrite($fh, "\n# Files and directories for local development\n"); 173 | fwrite($fh, $contents); 174 | 175 | fclose($fh); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /tests/config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'testing'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | your application so that it is used when running Artisan tasks. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | 'asset_url' => env('ASSET_URL', null), 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Application Timezone 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Here you may specify the default timezone for your application, which 65 | | will be used by the PHP date and date-time functions. We have gone 66 | | ahead and set this to a sensible default for you out of the box. 67 | | 68 | */ 69 | 70 | 'timezone' => 'UTC', 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Application Locale Configuration 75 | |-------------------------------------------------------------------------- 76 | | 77 | | The application locale determines the default locale that will be used 78 | | by the translation service provider. You are free to set this value 79 | | to any of the locales which will be supported by the application. 80 | | 81 | */ 82 | 83 | 'locale' => 'en', 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Application Fallback Locale 88 | |-------------------------------------------------------------------------- 89 | | 90 | | The fallback locale determines the locale to use when the current one 91 | | is not available. You may change the value to correspond to any of 92 | | the language folders that are provided through your application. 93 | | 94 | */ 95 | 96 | 'fallback_locale' => 'en', 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Faker Locale 101 | |-------------------------------------------------------------------------- 102 | | 103 | | This locale will be used by the Faker PHP library when generating fake 104 | | data for your database seeds. For example, this will be used to get 105 | | localized telephone numbers, street address information and more. 106 | | 107 | */ 108 | 109 | 'faker_locale' => 'en_US', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Encryption Key 114 | |-------------------------------------------------------------------------- 115 | | 116 | | This key is used by the Illuminate encrypter service and should be set 117 | | to a random, 32 character string, otherwise these encrypted strings 118 | | will not be safe. Please do this before deploying an application! 119 | | 120 | */ 121 | 122 | 'key' => env('APP_KEY'), 123 | 124 | 'cipher' => 'AES-256-CBC', 125 | 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | Logging Configuration 129 | |-------------------------------------------------------------------------- 130 | | 131 | | Here you may configure the log settings for your application. Out of 132 | | the box, Laravel uses the Monolog PHP logging library. This gives 133 | | you a variety of powerful log handlers / formatters to utilize. 134 | | 135 | | Available Settings: "single", "daily", "syslog" 136 | | 137 | */ 138 | 139 | 'log' => 'daily', 140 | 141 | 'log_level' => env('APP_LOG_LEVEL', 'debug'), 142 | 143 | /* 144 | |-------------------------------------------------------------------------- 145 | | Autoloaded Service Providers 146 | |-------------------------------------------------------------------------- 147 | | 148 | | The service providers listed here will be automatically loaded on the 149 | | request to your application. Feel free to add your own services to 150 | | this array to grant expanded functionality to your applications. 151 | | 152 | */ 153 | 154 | 'providers' => [ 155 | 156 | /* 157 | * Laravel Framework Service Providers... 158 | */ 159 | Illuminate\Auth\AuthServiceProvider::class, 160 | Illuminate\Broadcasting\BroadcastServiceProvider::class, 161 | Illuminate\Bus\BusServiceProvider::class, 162 | Illuminate\Cache\CacheServiceProvider::class, 163 | Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, 164 | Illuminate\Cookie\CookieServiceProvider::class, 165 | Illuminate\Database\DatabaseServiceProvider::class, 166 | Illuminate\Encryption\EncryptionServiceProvider::class, 167 | Illuminate\Filesystem\FilesystemServiceProvider::class, 168 | Illuminate\Foundation\Providers\FoundationServiceProvider::class, 169 | Illuminate\Hashing\HashServiceProvider::class, 170 | Illuminate\Mail\MailServiceProvider::class, 171 | Illuminate\Database\MigrationServiceProvider::class, 172 | Illuminate\Notifications\NotificationServiceProvider::class, 173 | Illuminate\Pagination\PaginationServiceProvider::class, 174 | Illuminate\Pipeline\PipelineServiceProvider::class, 175 | Gecche\Multidomain\Queue\QueueServiceProvider::class, 176 | Illuminate\Redis\RedisServiceProvider::class, 177 | Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, 178 | Illuminate\Session\SessionServiceProvider::class, 179 | Illuminate\Translation\TranslationServiceProvider::class, 180 | Illuminate\Validation\ValidationServiceProvider::class, 181 | Illuminate\View\ViewServiceProvider::class, 182 | 183 | ], 184 | 185 | /* 186 | |-------------------------------------------------------------------------- 187 | | Class Aliases 188 | |-------------------------------------------------------------------------- 189 | | 190 | | This array of class aliases will be registered when this application 191 | | is started. However, feel free to register as many as you wish as 192 | | the aliases are "lazy" loaded so they don't hinder performance. 193 | | 194 | */ 195 | 196 | 'aliases' => [ 197 | 198 | 'App' => Illuminate\Support\Facades\App::class, 199 | 'Arr' => Illuminate\Support\Arr::class, 200 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, 201 | 'Auth' => Illuminate\Support\Facades\Auth::class, 202 | 'Blade' => Illuminate\Support\Facades\Blade::class, 203 | 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, 204 | 'Bus' => Illuminate\Support\Facades\Bus::class, 205 | 'Cache' => Illuminate\Support\Facades\Cache::class, 206 | 'Config' => Illuminate\Support\Facades\Config::class, 207 | 'Cookie' => Illuminate\Support\Facades\Cookie::class, 208 | 'Crypt' => Illuminate\Support\Facades\Crypt::class, 209 | 'DB' => Illuminate\Support\Facades\DB::class, 210 | 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 211 | 'Event' => Illuminate\Support\Facades\Event::class, 212 | 'File' => Illuminate\Support\Facades\File::class, 213 | 'Gate' => Illuminate\Support\Facades\Gate::class, 214 | 'Hash' => Illuminate\Support\Facades\Hash::class, 215 | 'Http' => Illuminate\Support\Facades\Http::class, 216 | 'Lang' => Illuminate\Support\Facades\Lang::class, 217 | 'Log' => Illuminate\Support\Facades\Log::class, 218 | 'Mail' => Illuminate\Support\Facades\Mail::class, 219 | 'Notification' => Illuminate\Support\Facades\Notification::class, 220 | 'Password' => Illuminate\Support\Facades\Password::class, 221 | 'Queue' => Illuminate\Support\Facades\Queue::class, 222 | 'Redirect' => Illuminate\Support\Facades\Redirect::class, 223 | 'RedisManager' => Illuminate\Support\Facades\Redis::class, 224 | 'Request' => Illuminate\Support\Facades\Request::class, 225 | 'Response' => Illuminate\Support\Facades\Response::class, 226 | 'Route' => Illuminate\Support\Facades\Route::class, 227 | 'Schema' => Illuminate\Support\Facades\Schema::class, 228 | 'Session' => Illuminate\Support\Facades\Session::class, 229 | 'Storage' => Illuminate\Support\Facades\Storage::class, 230 | 'Str' => Illuminate\Support\Str::class, 231 | 'URL' => Illuminate\Support\Facades\URL::class, 232 | 'Validator' => Illuminate\Support\Facades\Validator::class, 233 | 'View' => Illuminate\Support\Facades\View::class, 234 | 235 | ], 236 | 237 | ]; 238 | -------------------------------------------------------------------------------- /src/Foundation/Application.php: -------------------------------------------------------------------------------- 1 | domainParams = $domainParams; 52 | $environmentPath = $environmentPath ?? $basePath; 53 | $this->useEnvironmentPath(rtrim($environmentPath,'\/')); 54 | 55 | parent::__construct($basePath); 56 | } 57 | 58 | /** 59 | * Begin configuring a new Laravel application instance. 60 | * 61 | * @param string|null $basePath 62 | * @param string|null $environmentPath 63 | * @param array $domainParams 64 | * @return \Gecche\Multidomain\Foundation\Configuration\ApplicationBuilder 65 | */ 66 | public static function configure(?string $basePath = null, ?string $environmentPath = null, array $domainParams = []) 67 | { 68 | $basePath = match (true) { 69 | is_string($basePath) => $basePath, 70 | default => static::inferBasePath(), 71 | }; 72 | 73 | return (new Configuration\ApplicationBuilder(new static($basePath, $environmentPath, $domainParams))) 74 | ->withKernels() 75 | ->withEvents() 76 | ->withCommands() 77 | ->withProviders(); 78 | } 79 | /** 80 | * Detect the application's current domain. 81 | * 82 | * @param array|string $envs 83 | * @return void; 84 | */ 85 | public function detectDomain() 86 | { 87 | 88 | $args = isset($_SERVER['argv']) ? $_SERVER['argv'] : null; 89 | 90 | $domainDetectionFunctionWeb = Arr::get($this->domainParams,'domain_detection_function_web'); 91 | $domainDetector = new DomainDetector($domainDetectionFunctionWeb); 92 | $fullDomain = $domainDetector->detect($args); 93 | list($domain_scheme, $domain_name, $domain_port) = $domainDetector->split($fullDomain); 94 | $this['full_domain'] = $fullDomain; 95 | $this['domain'] = $domain_name; 96 | $this['domain_scheme'] = $domain_scheme; 97 | $this['domain_port'] = $domain_port; 98 | 99 | $this->domainDetected = true; 100 | return; 101 | } 102 | 103 | 104 | /** 105 | * Force the detection of the domain if it has never been detected. 106 | * It should not happens in standard flow. 107 | * 108 | * @return void; 109 | */ 110 | protected function checkDomainDetection() 111 | { 112 | if (!$this->domainDetected) 113 | $this->detectDomain(); 114 | return; 115 | } 116 | 117 | /** 118 | * Get or check the current application domain. 119 | * 120 | * @return string 121 | */ 122 | public function domain() 123 | { 124 | 125 | $this->checkDomainDetection(); 126 | 127 | if (count(func_get_args()) > 0) { 128 | return in_array($this['domain'], func_get_args()); 129 | } 130 | 131 | return $this['domain']; 132 | } 133 | 134 | /** 135 | * Get or check the full current application domain with HTTP scheme and port. 136 | * 137 | * @param mixed 138 | * @return string 139 | */ 140 | public function fullDomain() 141 | { 142 | $this->checkDomainDetection(); 143 | 144 | if (count(func_get_args()) > 0) { 145 | return in_array($this['full_domain'], func_get_args()); 146 | } 147 | 148 | return $this['full_domain']; 149 | } 150 | 151 | /** 152 | * Get the environment file the application is using. 153 | * 154 | * @return string 155 | */ 156 | public function environmentFile() 157 | { 158 | return $this->environmentFile ?: $this->environmentFileDomain(); 159 | } 160 | 161 | /** 162 | * Get the environment file of the current domain if it exists. 163 | * The file has to be named .env. 164 | * It returns the base .env file if a specific file does not exist. 165 | * 166 | * @return string 167 | */ 168 | public function environmentFileDomain($domain = null) 169 | { 170 | $this->checkDomainDetection(); 171 | 172 | $domain = $domain ?? $this['domain'] ?? ''; 173 | 174 | $envFile = $this->searchForEnvFileDomain(explode('.',$domain)); 175 | 176 | return $envFile; 177 | 178 | } 179 | 180 | protected function searchForEnvFileDomain($tokens = []) { 181 | $tokens = array_filter($tokens); 182 | if (count($tokens) == 0) { 183 | return '.env'; 184 | } 185 | 186 | $file = '.env.' . implode('.',$tokens); 187 | return file_exists(env_path($file)) 188 | ? $file 189 | : $this->searchForEnvFileDomain(array_splice($tokens,1)); 190 | } 191 | 192 | /** 193 | * Get the path to the storage directory of the current domain. 194 | * The storage path is a folder in the main storage laravel folder 195 | * with the sanitized domain name (dots are replaced with underscores) 196 | * It is sanitized in order to avoid problems with dots in paths especially 197 | * in the case of using array_dot notation. 198 | * 199 | * @return string 200 | */ 201 | public function domainStoragePath($domain = null) 202 | { 203 | 204 | $this->checkDomainDetection(); 205 | 206 | if (is_null($domain)) { 207 | $domain = $this['domain']; 208 | } 209 | 210 | $domainStoragePath = $this->searchForDomainStoragePath(parent::storagePath(),explode('.',$domain)); 211 | 212 | $this->domainStoragePath = $domainStoragePath; 213 | 214 | return $domainStoragePath; 215 | 216 | } 217 | 218 | 219 | /* 220 | * Returns the exact storage path based on the domain (useful for package commands, could not exists) 221 | * 222 | * @return string 223 | */ 224 | public function exactDomainStoragePath($domain = null) 225 | { 226 | $this->checkDomainDetection(); 227 | 228 | if (is_null($domain)) { 229 | $domain = $this['domain']; 230 | } 231 | 232 | return rtrim(parent::storagePath() . DIRECTORY_SEPARATOR . domain_sanitized($domain),DIRECTORY_SEPARATOR); 233 | } 234 | 235 | /* 236 | * Laravel storagePath updated with domains. 237 | * 238 | * @return string 239 | */ 240 | public function storagePath($path = '', $domain = null) 241 | { 242 | 243 | $storagePath = $this->storagePath ?: ($this->domainStoragePath ?: $this->domainStoragePath($domain)); 244 | 245 | return $storagePath.($path != '' ? DIRECTORY_SEPARATOR.$path : ''); 246 | } 247 | 248 | 249 | protected function searchForDomainStoragePath($storagePath, $tokens = []) { 250 | if (count($tokens) == 0) { 251 | return $storagePath; 252 | } 253 | 254 | $tokensAsDomainString = implode('.',$tokens); 255 | 256 | $domainStoragePath = rtrim($storagePath . DIRECTORY_SEPARATOR . domain_sanitized($tokensAsDomainString), "\/"); 257 | return file_exists($domainStoragePath) 258 | ? $domainStoragePath 259 | : $this->searchForDomainStoragePath($storagePath,array_splice($tokens,1)); 260 | } 261 | 262 | 263 | /** 264 | * Get the path to the configuration cache file. 265 | * 266 | * @return string 267 | */ 268 | public function getCachedConfigPath() 269 | { 270 | return $this->normalizeCacheDomainPath('APP_CONFIG_CACHE', 'cache/config.php'); 271 | } 272 | 273 | /** 274 | * Get the path to the configuration cache file. 275 | * 276 | * @return string 277 | */ 278 | public function getCachedRoutesPath() 279 | { 280 | return $this->normalizeCacheDomainPath('APP_ROUTES_CACHE', 'cache/routes.php'); 281 | } 282 | 283 | /** 284 | * Get the path to the events cache file. 285 | * 286 | * @return string 287 | */ 288 | public function getCachedEventsPath() 289 | { 290 | return $this->normalizeCacheDomainPath('APP_EVENTS_CACHE', 'cache/events.php'); 291 | } 292 | 293 | /** 294 | * Get the path to the default cache file for config or routes. 295 | * 296 | * @param string 297 | * @return string 298 | */ 299 | protected function normalizeCacheDomainPath($key, $default) 300 | { 301 | 302 | if (is_null($env = Env::get($key))) { 303 | $domainDefault = $this->getDomainCachedFileDefault($default); 304 | return $this->bootstrapPath($domainDefault); 305 | } 306 | 307 | return Str::startsWith($env, '/') 308 | ? $env 309 | : $this->basePath($env); 310 | 311 | } 312 | 313 | /** 314 | * Get a default file for cache files depending upon the loaded .env file 315 | * 316 | * @return string 317 | */ 318 | protected function getDomainCachedFileDefault($default) 319 | { 320 | $envFile = $this->environmentFile(); 321 | if ($envFile && $envFile == '.env') 322 | return $default; 323 | 324 | $this->checkDomainDetection(); 325 | 326 | $defaultWithoutPhpExt = substr($default,0,-4); 327 | 328 | return $defaultWithoutPhpExt.'-'.domain_sanitized($this['domain']).'.php'; 329 | } 330 | 331 | 332 | /* 333 | * Get the list of installed domains 334 | * 335 | * @return Array 336 | */ 337 | public function domainsList() 338 | { 339 | 340 | $domainsInConfig = config('domain.domains', []); 341 | 342 | $domains = []; 343 | 344 | foreach ($domainsInConfig as $domain) { 345 | $domains[$domain] = [ 346 | 'storage_path' => $this->domainStoragePath($domain), 347 | 'env' => $this->environmentFileDomain($domain), 348 | ]; 349 | } 350 | 351 | return $domains; 352 | 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /tests/src/CommandsTestCase.php: -------------------------------------------------------------------------------- 1 | setPaths(); 56 | 57 | $this->files = new Filesystem(); 58 | 59 | if (!is_dir(env_path())) { 60 | mkdir(env_path()); 61 | } 62 | 63 | copy(__DIR__ . '/../.env.example',env_path('.env')); 64 | 65 | $this->artisan('vendor:publish',['--provider' => 'Gecche\Multidomain\Foundation\Providers\DomainConsoleServiceProvider']); 66 | 67 | 68 | } 69 | 70 | protected function setPaths() 71 | { 72 | $this->laravelAppPath = __DIR__ . '/../../vendor/orchestra/testbench-core/laravel'; 73 | $this->laravelEnvPath = $this->laravelAppPath; 74 | } 75 | 76 | /** 77 | * Resolve application Console Kernel implementation. 78 | * 79 | * @param \Illuminate\Foundation\Application $app 80 | * @return void 81 | */ 82 | protected function resolveApplicationConsoleKernel($app) 83 | { 84 | $app->singleton(ConsoleKernelContract::class, ConsoleKernel::class); 85 | } 86 | 87 | protected function resolveApplicationHttpKernel($app) 88 | { 89 | $app->singleton(HttpKernelContract::class, HttpKernel::class); 90 | } 91 | 92 | protected function resolveApplication() 93 | { 94 | static::$cacheApplicationBootstrapFile ??= $this->getApplicationBootstrapFile('app.php'); 95 | 96 | if (\is_string(static::$cacheApplicationBootstrapFile)) { 97 | $APP_BASE_PATH = $_ENV['APP_BASE_PATH']; 98 | 99 | return require static::$cacheApplicationBootstrapFile; 100 | } 101 | 102 | return $this->resolveGeccheApplication(); 103 | } 104 | 105 | final protected function resolveGeccheApplication() 106 | { 107 | return (new ApplicationBuilder(new Application($_ENV['APP_BASE_PATH']))) 108 | ->withProviders() 109 | ->withMiddleware(static function ($middleware) { 110 | // 111 | }) 112 | ->withCommands() 113 | ->create(); 114 | } 115 | 116 | /** 117 | * Define environment setup. 118 | * 119 | * @param \Illuminate\Foundation\Application $app 120 | * @return void 121 | */ 122 | protected function getEnvironmentSetUp($app) 123 | { 124 | // set up database configuration 125 | } 126 | 127 | /** 128 | * Get Sluggable package providers. 129 | * 130 | * @return array 131 | */ 132 | protected function getPackageProviders($app) 133 | { 134 | return [ 135 | TestServiceProvider::class, 136 | DomainConsoleServiceProvider::class, 137 | // ServiceProvider::class, 138 | ]; 139 | } 140 | 141 | 142 | /* 143 | * TEST FOR DOMAIN COMMAND 144 | * First we add the domain adn we check is in the output of the domain:list command. 145 | * Then we remove the domain adn we check is no more in the output of the domain:list command. 146 | */ 147 | public function testDomainCommand() { 148 | 149 | 150 | $serverName = Arr::get($_SERVER,'SERVER_NAME',''); 151 | 152 | $this->artisan('domain'); 153 | 154 | $artisanOutput = Artisan::output(); 155 | 156 | //CHECK IS IN THE OUTPUT OF THE COMMAND 157 | $this->assertStringContainsString($serverName,$artisanOutput); 158 | 159 | 160 | } 161 | 162 | /* 163 | * TEST FOR DOMAIN ADD COMMAND 164 | * It checks if the env file and storage dirs exist and if the list of domains in the config file is updated 165 | */ 166 | public function testDomainAddCommand() 167 | { 168 | $site = Arr::get($_SERVER, 'SERVER_NAME'); 169 | 170 | if (!$site) { 171 | $this->assertTrue(true); 172 | return; 173 | } 174 | 175 | $argDomain = $site ? ['domain' => $site] : []; 176 | 177 | $this->artisan('domain:add', $argDomain); 178 | 179 | $this->assertFileExists(env_path('.env.'.$site)); 180 | 181 | $this->artisan('config:clear'); 182 | 183 | $domainListed = Config::get('domain.domains'); 184 | 185 | $this->assertArrayHasKey($site,$domainListed); 186 | 187 | $this->assertDirectoryExists(app()->exactDomainStoragePath()); 188 | } 189 | 190 | /* 191 | * TEST FOR DOMAIN REMOVE COMMAND 192 | * It checks if the .env file does not exist and if the list of domains in the config file is updated without the domain. 193 | * It checks also if storage dirs still exist 194 | */ 195 | public function testDomainRemoveCommand() 196 | { 197 | $site = Arr::get($_SERVER, 'SERVER_NAME'); 198 | if (!$site) { 199 | $this->assertTrue(true); 200 | return; 201 | } 202 | $argDomain = $site ? ['domain' => $site] : []; 203 | 204 | $this->artisan('domain:remove', $argDomain); 205 | 206 | $this->assertFileDoesNotExist(env_path('.env.'.$site)); 207 | 208 | $domainListed = Config::get('domain.domains'); 209 | 210 | $this->assertArrayNotHasKey($site,$domainListed); 211 | 212 | $this->assertDirectoryExists(app()->exactDomainStoragePath()); 213 | } 214 | 215 | /* 216 | * TEST FOR DOMAIN REMOVE COMMAND (FORCE) 217 | * It checks if the .env file does not exist and if the list of domains in the config file is updated without the domain. 218 | * Now it checks also if storage dirs does not exist (force) 219 | */ 220 | public function testDomainRemoveForceCommand() 221 | { 222 | $site = Arr::get($_SERVER, 'SERVER_NAME'); 223 | if (!$site) { 224 | $this->assertTrue(true); 225 | return; 226 | } 227 | $argDomain = $site ? ['domain' => $site, '--force' => 1] : ['--force' => 1]; 228 | 229 | $this->artisan('domain:remove', $argDomain); 230 | 231 | $this->assertFileDoesNotExist(env_path('.env.'.$site)); 232 | 233 | $domainListed = Config::get('domain.domains'); 234 | 235 | $this->assertArrayNotHasKey($site,$domainListed); 236 | 237 | //$this->assertDirectoryDoesNotExist(storage_path(domain_sanitized($site))); 238 | $this->assertDirectoryDoesNotExist(app()->exactDomainStoragePath()); 239 | } 240 | 241 | /* 242 | * TEST FOR DOMAIN REMOVE COMMAND (FORCE) 243 | * It checks if the .env file does not exist and if the list of domains in the config file is updated without the domain. 244 | * Now it checks also if storage dirs does not exist (force) 245 | */ 246 | public function testDomainRemoveForceCommandSubsite() 247 | { 248 | 249 | $mainStoragePath = $this->laravelAppPath . DIRECTORY_SEPARATOR . 'storage'; 250 | $sites = [ 251 | 'site1.com', 252 | 'sub1.site1.com', 253 | 'sub2.site1.com', 254 | ]; 255 | foreach ($sites as $currSite) { 256 | $this->artisan('domain:add', ['domain' => $currSite]); 257 | $this->assertFileExists(env_path('.env.'.$currSite)); 258 | $this->assertDirectoryExists($mainStoragePath . DIRECTORY_SEPARATOR . domain_sanitized($currSite)); 259 | } 260 | 261 | 262 | $site = 'sub1.site1.com'; 263 | $argDomain = ['domain' => $site, '--force' => 1]; 264 | $this->artisan('domain:remove', $argDomain); 265 | 266 | foreach ($sites as $currSite) { 267 | if ($site == $currSite) { 268 | $this->assertFileDoesNotExist(env_path('.env.'.$currSite)); 269 | $this->assertDirectoryDoesNotExist($mainStoragePath . DIRECTORY_SEPARATOR . domain_sanitized($currSite)); 270 | } else { 271 | $this->assertFileExists(env_path('.env.'.$currSite)); 272 | $this->assertDirectoryExists($mainStoragePath . DIRECTORY_SEPARATOR . domain_sanitized($currSite)); 273 | } 274 | } 275 | 276 | foreach ($sites as $currSite) { 277 | if ($site != $currSite) { 278 | $this->artisan('domain:remove', ['domain' => $currSite,'--force' => 1]); 279 | $this->assertFileDoesNotExist(env_path('.env.'.$currSite)); 280 | $this->assertDirectoryDoesNotExist($mainStoragePath . DIRECTORY_SEPARATOR . domain_sanitized($currSite)); 281 | } 282 | } 283 | 284 | } 285 | 286 | /* 287 | * TEST FOR DOMAIN UPDATE_ENV COMMAND 288 | * First we remove and add the domain to be sure is fresh. 289 | * Then we check the DB_DATABASE value in the corresponding .env.. 290 | * We update the DB_DATABASE value and then we check the final value. 291 | */ 292 | public function testDomainUpdateEnvCommand() { 293 | 294 | $dbName = $this->siteDbName; 295 | $site = Arr::get($_SERVER, 'SERVER_NAME'); 296 | if (!$site) { 297 | $this->assertTrue(true); 298 | return; 299 | } 300 | $argDomain = $site ? ['domain' => $site] : []; 301 | 302 | $this->artisan('domain:remove', array_merge($argDomain, ['--force' => 1])); 303 | $this->artisan('domain:add', $argDomain); 304 | 305 | $fileContents = explode("\n",$this->files->get(env_path('.env.'.$site))); 306 | $this->assertNotContains("DB_DATABASE=".$dbName,$fileContents); 307 | 308 | $this->artisan('domain:update_env', array_merge($argDomain, [ 309 | '--domain_values' => '{"DB_DATABASE":"'.$dbName.'"}', 310 | ])); 311 | 312 | $fileContents = explode("\n",$this->files->get(env_path('.env.'.$site))); 313 | $this->assertContains("DB_DATABASE=".$dbName,$fileContents); 314 | 315 | $this->artisan('domain:remove', array_merge($argDomain, ['--force' => 1])); 316 | 317 | } 318 | 319 | /* 320 | * TEST FOR DOMAIN LIST COMMAND 321 | * First we add the domain adn we check is in the output of the domain:list command. 322 | * Then we remove the domain adn we check is no more in the output of the domain:list command. 323 | */ 324 | public function testDomainListCommand() { 325 | 326 | $site = Arr::get($_SERVER, 'SERVER_NAME'); 327 | $argDomain = $site ? ['domain' => $site] : []; 328 | 329 | //ADD THE DOMAIN 330 | if ($site) { 331 | $this->artisan('domain:add', $argDomain); 332 | } 333 | 334 | $this->artisan('domain:list'); 335 | 336 | $artisanOutput = Artisan::output(); 337 | 338 | //CHECK IS IN THE OUTPUT OF THE COMMAND 339 | if ($site) { 340 | $this->assertStringContainsString($site,$artisanOutput); 341 | } else { 342 | $this->assertEquals($site, $artisanOutput); 343 | } 344 | 345 | 346 | //REMOVE THE DOMAIN 347 | if ($site) { 348 | $this->artisan('domain:remove', array_merge($argDomain, ['--force' => 1])); 349 | } 350 | $this->artisan('domain:list'); 351 | 352 | $artisanOutput = Artisan::output(); 353 | 354 | //CHECK IS NOT IN THE OUTPUT OF THE COMMAND 355 | if ($site) { 356 | $this->assertStringNotContainsString($site,$artisanOutput); 357 | } else { 358 | $this->assertEquals($site, $artisanOutput); 359 | } 360 | } 361 | 362 | 363 | } -------------------------------------------------------------------------------- /tests/src/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | laravelAppPath = __DIR__ . '/../../vendor/orchestra/testbench-core/laravel'; 78 | copy(__DIR__ . '/../artisan',$this->laravelAppPath.'/artisan'); 79 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'config:clear']); 80 | $process->run(); 81 | 82 | 83 | $this->files = new Filesystem(); 84 | if (!file_exists($this->laravelAppPath.'/config/domain.php')) { 85 | file_put_contents($this->laravelAppPath.'/config/domain.php','laravelAppPath.'/config/app.php',$this->laravelAppPath.'/config/appORIG.php'); 88 | copy(__DIR__ . '/../config/app.php',$this->laravelAppPath.'/config/app.php'); 89 | copy(__DIR__ . '/../.env.example', $this->laravelAppPath.'/.env'); 90 | 91 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'vendor:publish', '--provider="Gecche\Multidomain\Foundation\Providers\DomainConsoleServiceProvider"']); 92 | $process->run(); 93 | 94 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:remove', $this->site1, '--force']); 95 | $process->run(); 96 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:remove', $this->site2, '--force']); 97 | $process->run(); 98 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:remove', $this->subSite1, '--force']); 99 | $process->run(); 100 | 101 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:add', $this->site1]); 102 | $process->run(); 103 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:add', $this->site2]); 104 | $process->run(); 105 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:add', $this->subSite1]); 106 | $process->run(); 107 | 108 | $domainValues = [ 109 | 'APP_NAME' => $this->siteAppName1, 110 | 'DB_DATABASE' => $this->siteDbName1, 111 | ]; 112 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:update_env', $this->site1, '--domain_values='.json_encode($domainValues)]); 113 | $process->run(); 114 | 115 | $domainValues = [ 116 | 'APP_NAME' => $this->siteAppName2, 117 | 'DB_DATABASE' => $this->siteDbName2, 118 | ]; 119 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:update_env', $this->site2, '--domain_values='.json_encode($domainValues)]); 120 | $process->run(); 121 | 122 | $domainValues = [ 123 | 'APP_NAME' => $this->subSiteAppName1, 124 | 'DB_DATABASE' => $this->subSiteDbName1, 125 | ]; 126 | $process = new Process(['php', $this->laravelAppPath.'/artisan', 'domain:update_env', $this->subSite1, '--domain_values='.json_encode($domainValues)]); 127 | $process->run(); 128 | 129 | parent::setUp(); 130 | 131 | 132 | } 133 | 134 | protected function tearDown(): void 135 | { 136 | $this->artisan('domain:remove', ['domain' => $this->site1, '--force' => 1]); 137 | $this->artisan('domain:remove', ['domain' => $this->site2, '--force' => 1]); 138 | $this->artisan('domain:remove', ['domain' => $this->subSite1, '--force' => 1]); 139 | parent::tearDown(); // TODO: Change the autogenerated stub 140 | 141 | } 142 | 143 | 144 | /** 145 | * Resolve application Console Kernel implementation. 146 | * 147 | * @param \Illuminate\Foundation\Application $app 148 | * @return void 149 | */ 150 | protected function resolveApplicationConsoleKernel($app) 151 | { 152 | $app->singleton(ConsoleKernelContract::class, ConsoleKernel::class); 153 | } 154 | 155 | protected function resolveApplicationHttpKernel($app) 156 | { 157 | $app->singleton(HttpKernelContract::class, HttpKernel::class); 158 | } 159 | 160 | protected function resolveApplication() 161 | { 162 | static::$cacheApplicationBootstrapFile ??= $this->getApplicationBootstrapFile('app.php'); 163 | 164 | if (\is_string(static::$cacheApplicationBootstrapFile)) { 165 | $APP_BASE_PATH = $_ENV['APP_BASE_PATH']; 166 | 167 | return require static::$cacheApplicationBootstrapFile; 168 | } 169 | 170 | return $this->resolveGeccheApplication(); 171 | } 172 | 173 | final protected function resolveGeccheApplication() 174 | { 175 | return (new ApplicationBuilder(new Application($_ENV['APP_BASE_PATH']))) 176 | ->withProviders() 177 | ->withMiddleware(static function ($middleware) { 178 | // 179 | }) 180 | ->withCommands() 181 | ->create(); 182 | } 183 | 184 | /** 185 | * Define environment setup. 186 | * 187 | * @param \Illuminate\Foundation\Application $app 188 | * @return void 189 | */ 190 | protected function getEnvironmentSetUp($app) 191 | { 192 | // set up database configuration 193 | } 194 | 195 | /** 196 | * Get Sluggable package providers. 197 | * 198 | * @return array 199 | */ 200 | protected function getPackageProviders($app) 201 | { 202 | return [ 203 | TestServiceProvider::class, 204 | DomainConsoleServiceProvider::class, 205 | // ServiceProvider::class, 206 | ]; 207 | } 208 | 209 | 210 | /** 211 | * In this test we simply checks that in the route "/" we display 212 | * the right APP_NAME from the env file depending upon the $_SERVER['SERVER_NAME'] value 213 | * set when the test has been launched. 214 | * 215 | */ 216 | public function testWelcomePage() 217 | { 218 | 219 | $this->serverName = Arr::get($_SERVER, 'SERVER_NAME'); 220 | $stringToSee = 'Laravel'; 221 | if (in_array($this->serverName, [$this->subSite1])) { 222 | $stringToSee = $this->subSiteAppName1; 223 | } elseif (in_array($this->serverName, [$this->site1]) || Str::endsWith($this->serverName, '.'.$this->site1)) { 224 | $stringToSee = $this->siteAppName1; 225 | } elseif (in_array($this->serverName, [$this->site2]) || Str::endsWith($this->serverName, '.'.$this->site2)) { 226 | $stringToSee = $this->siteAppName2; 227 | } 228 | 229 | $this->visit('http://' . $this->serverName) 230 | ->see($stringToSee); 231 | } 232 | 233 | 234 | /** 235 | * In this test we checks that the database connection is versus the right database set in the 236 | * DB_DATABASE entry of the env file. 237 | * The chosen env files again depends upon the $_SERVER['SERVER_NAME'] value 238 | * set when the test has been launched. 239 | * 240 | */ 241 | public function testDBConnection() 242 | { 243 | 244 | $this->serverName = Arr::get($_SERVER, 'SERVER_NAME'); 245 | $dbName = DB::connection('mysql')->getDatabaseName(); 246 | $expectedDb = 'homestead'; 247 | if (in_array($this->serverName, [$this->subSite1])) { 248 | $expectedDb = $this->subSiteDbName1; 249 | } elseif (in_array($this->serverName, [$this->site1]) || Str::endsWith($this->serverName, '.'.$this->site1)) { 250 | $expectedDb = $this->siteDbName1; 251 | } elseif (in_array($this->serverName, [$this->site2]) || Str::endsWith($this->serverName, '.'.$this->site2)) { 252 | $expectedDb = $this->siteDbName2; 253 | } 254 | 255 | $this->assertEquals($expectedDb, $dbName); 256 | } 257 | 258 | 259 | /** 260 | * In this test we checks that the database connection is versus the right database set in the 261 | * DB_DATABASE entry of the env file. 262 | * The chosen env files again depends upon the $_SERVER['SERVER_NAME'] value 263 | * set when the test has been launched. 264 | * 265 | */ 266 | public function testEnvFile() 267 | { 268 | 269 | $this->serverName = Arr::get($_SERVER, 'SERVER_NAME'); 270 | $envfileName = app()->environmentFile(); 271 | $expectedEnvFile = '.env'; 272 | if (in_array($this->serverName, [$this->subSite1])) { 273 | $expectedEnvFile = '.env.'.$this->subSite1; 274 | } elseif (in_array($this->serverName, [$this->site1]) || Str::endsWith($this->serverName, '.'.$this->site1)) { 275 | $expectedEnvFile = '.env.'.$this->site1; 276 | } elseif (in_array($this->serverName, [$this->site2]) || Str::endsWith($this->serverName, '.'.$this->site2)) { 277 | $expectedEnvFile = '.env.'.$this->site2; 278 | } 279 | 280 | $this->assertEquals($expectedEnvFile, $envfileName); 281 | } 282 | 283 | /** 284 | * In this test we checks that the database connection is versus the right database set in the 285 | * DB_DATABASE entry of the env file. 286 | * The chosen env files again depends upon the $_SERVER['SERVER_NAME'] value 287 | * set when the test has been launched. 288 | * 289 | */ 290 | public function testStorageFolder() 291 | { 292 | 293 | $this->serverName = Arr::get($_SERVER, 'SERVER_NAME'); 294 | $storageFolder = storage_path(); 295 | $expectedStorageFolder = base_path() . '/storage'; 296 | 297 | if (in_array($this->serverName, [$this->subSite1])) { 298 | $expectedStorageFolder = $expectedStorageFolder . DIRECTORY_SEPARATOR . domain_sanitized($this->subSite1); 299 | } elseif (in_array($this->serverName, [$this->site1]) || Str::endsWith($this->serverName, '.'.$this->site1)) { 300 | $expectedStorageFolder = $expectedStorageFolder . DIRECTORY_SEPARATOR . domain_sanitized($this->site1); 301 | } elseif (in_array($this->serverName, [$this->site2]) || Str::endsWith($this->serverName, '.'.$this->site2)) { 302 | $expectedStorageFolder = $expectedStorageFolder . DIRECTORY_SEPARATOR . domain_sanitized($this->site2); 303 | } 304 | 305 | $this->assertEquals($expectedStorageFolder, $storageFolder); 306 | } 307 | 308 | 309 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://tldrlegal.com/license/mit-license) 2 | [![Laravel](https://img.shields.io/badge/Laravel-12.x-orange.svg?style=flat-square)](http://laravel.com) 3 | [![Laravel](https://img.shields.io/badge/Laravel-11.x-orange.svg?style=flat-square)](http://laravel.com) 4 | [![Laravel](https://img.shields.io/badge/Laravel-10.x-orange.svg?style=flat-square)](http://laravel.com) 5 | [![Laravel](https://img.shields.io/badge/Laravel-9.x-orange.svg?style=flat-square)](http://laravel.com) 6 | [![Laravel](https://img.shields.io/badge/Laravel-8.x-orange.svg?style=flat-square)](http://laravel.com) 7 | [![Laravel](https://img.shields.io/badge/Laravel-7.x-orange.svg?style=flat-square)](http://laravel.com) 8 | [![Laravel](https://img.shields.io/badge/Laravel-6.x-orange.svg?style=flat-square)](http://laravel.com) 9 | [![Laravel](https://img.shields.io/badge/Laravel-5.x-orange.svg?style=flat-square)](http://laravel.com) 10 | 11 | # Laravel Multi Domain 12 | An extension for using Laravel in a multi domain setting 13 | 14 | ![Laravel Multi Domain](laravel-multidomain.png) 15 | 16 | ## Description 17 | This package allows a single Laravel installation to work with multiple HTTP domains. 18 | 19 | There are many cases in which different customers use the same application in terms of code but not in terms of 20 | database, storage and configuration. 21 | 22 | This package gives a very simple way to get a specific env file, a specific storage path and a specific database 23 | for each such customer. 24 | 25 | ## Documentation 26 | 27 | ### Version Compatibility 28 | 29 | Laravel | Multidomain 30 | :----------|:----------- 31 | 12.x | 12.x 32 | 11.x | 11.x 33 | 10.x | 10.x 34 | 9.x | 5.x 35 | 8.x | 4.x 36 | 7.x | 3.x 37 | 6.x | 2.x 38 | 5.8.x | 1.4.x 39 | 5.7.x | 1.3.x 40 | 5.6.x | 1.2.x 41 | 5.5.x | 1.1.x 42 | 43 | #### Further notes on Compatibility 44 | 45 | Releases v1.1.x: 46 | - From v1.1.0 to v1.1.5, releases are fully compatibile with Laravel 5.5, 5.6, 5.7, 5.8 or 6.0. 47 | - From v1.1.6+ releases v1.1.x are only compatible with Laravel 5.5 in order to run tests correctly. 48 | 49 | To date, releases v1.1.6+, v1.2.x, v1.3.x, v1.4.x, v2.x and v3.x are functionally equivalent. 50 | Releases have been separated in order to run integration tests with the corresponding version of the 51 | Laravel framework. 52 | 53 | However, with the release of Laravel 8, releases v1.1.14, v1.2.8, v1.3.8 and v1.4.8 are the last releases 54 | including new features for the corresponding Laravel 5.x versions (bugfix support is still active for that versions). 55 | **2021-02-13 UPDATE**: some last features for v1.1+ releases are still ongoing :) 56 | 57 | v1.0 requires Laravel 5.1, 5.2, 5.3 and 5.4 (no longer maintained and not tested versus laravel 5.4, 58 | however the usage of the package is the same as for 1.1) 59 | 60 | **2023-02-20 UPDATE**: From Laravel 10.x up, the package versions follow the same numbering. 61 | 62 | ### Installation 63 | 64 | Add gecche/laravel-multidomain as a requirement to composer.json: 65 | 66 | ```javascript 67 | { 68 | "require": { 69 | "gecche/laravel-multidomain": "12.*" 70 | } 71 | } 72 | ``` 73 | 74 | Update your packages with composer update or install with composer install. 75 | 76 | You can also add the package using `composer require gecche/laravel-multidomain` and later 77 | specify the version you want. 78 | 79 | This package needs to override the detection of the HTTP domain in a minimal set of Laravel core functions 80 | at the very start of the bootstrap process in order to get the specific environment file. So this package 81 | needs a few more configuration steps than most Laravel packages. 82 | 83 | Installation steps: 84 | 1. replace the whole Laravel container by modifying the following lines 85 | at the very top of the `bootstrap/app.php` file. 86 | 87 | ```php 88 | //use Illuminate\Foundation\Application 89 | use Gecche\Multidomain\Foundation\Application 90 | ``` 91 | 92 | 2. Override the `QueueServiceProvider` with the extended 93 | one in the `config/app.php` file as follows: 94 | 95 | ```php 96 | 'providers' => \Illuminate\Support\ServiceProvider::defaultProviders()->merge([ 97 | // Package Service Providers... 98 | ])->replace([ 99 | \Illuminate\Queue\QueueServiceProvider::class => \Gecche\Multidomain\Queue\QueueServiceProvider::class, 100 | ])->merge([ 101 | // Added Service Providers (Do not remove this line)... 102 | ])->toArray(), 103 | ``` 104 | 105 | Please note that if you changed the `config/app.php` file due to other reasons, probably there is already 106 | the above `providers` entry in that file and the only important line is the one which replaces the `QueueServiceProvider`. 107 | 108 | 3. Publish the config file. 109 | 110 | ``` 111 | php artisan vendor:publish 112 | ``` 113 | 114 | (This package makes use of the discovery feature.) 115 | 116 | Following the above steps, your application will be aware of the HTTP domain 117 | in which is running, both for HTTP and CLI requests, including queue support. 118 | 119 | NOTE: in Laravel 11 the installation is simpler than before: if you use a 120 | previous version of Laravel, please check in the documentation the installation steps. 121 | 122 | #### Laravel Horizon installation: 123 | 124 | The package is compatible with Horizon, thatnks to community contributions. If you need to use 125 | this package together with Horizon you have to follow other two installation steps: 126 | 127 | 1. Install Laravel Horizon as usual 128 | 129 | 2. Replace the Laravel Horizon import at the very top of the app/Providers/HorizonServiceProvider.php file. 130 | 131 | ```php 132 | //use Laravel\Horizon\HorizonApplicationServiceProvider; 133 | use Gecche\Multidomain\Horizon\HorizonApplicationServiceProvider; 134 | ``` 135 | 136 | ### Usage 137 | 138 | This package adds three commands to manage your application HTTP domains: 139 | 140 | #### `domain.add` artisan command 141 | 142 | The main command is the `domain:add` command which takes as argument the name of the 143 | HTTP domain to add to the application. Let us suppose we have two domains, `site1.com` 144 | and `site2.com`, sharing the same code. 145 | 146 | We simply do: 147 | 148 | ``` 149 | php artisan domain:add site1.com 150 | ``` 151 | 152 | and 153 | 154 | ``` 155 | php artisan domain:add site2.com 156 | ``` 157 | 158 | These commands create two new environment files, `.env.site1.com` and `.env.site2.com`, 159 | in which you can put the specific configuration for each site (e.g. databases configuration, 160 | cache configuration and other configurations, as usually found in an environment file). 161 | 162 | The command also adds an entry in the `domains` key in `config/domains.php` file. 163 | 164 | In addition, two new folders are created, `storage/site1_com/` and `storage/site2_com/`. 165 | They have the same folder structure as the main storage. 166 | 167 | Customizations to this `storage` substructure must be matched by values in the `config/domain.php` file. 168 | 169 | #### `domain.remove` artisan command 170 | The `domain:remove` command removes the specified HTTP domain from the 171 | application by deleting its environment file. E.g.: 172 | 173 | ``` 174 | php artisan domain:remove site2.com 175 | ``` 176 | Adding the `force` option will delete the domain storage folder. 177 | 178 | The command also removes the appropriate entry from, the `domains` key in `config/domains.php` file. 179 | 180 | #### `domain.update_env` artisan command 181 | The `domain:update_env` command passes a json encoded array of data to update one or all of the environment files. 182 | These values will be added at the end of the appropriate .env. 183 | 184 | Update a single domain environment file by adding the `domain` argument. 185 | 186 | When the `domain` argument is absent, the command updates all the environment files, including the standard `.env` one. 187 | 188 | The list of domains to be updated is maintained in the `domain.php` config file. 189 | 190 | E.g.: 191 | 192 | ``` 193 | php artisan domain:update_env --domain_values='{"TOM_DRIVER":"TOMMY"}' 194 | ``` 195 | 196 | will add the line `TOM_DRIVER=TOMMY` to all the domain environment files. 197 | 198 | #### `domain.list` artisan command 199 | The `domain:list` command lists the currently installed domains, with their .env file and storage path dir. 200 | 201 | The list is maintained in the `domains` key of the `config/domain.php` config file. 202 | 203 | This list is automatically updated at every `domain:add` and `domain:remove` commands run. 204 | 205 | #### `config:cache` artisan command 206 | The config:cache artisan command can be used with this package in the same way as any other 207 | artisan command. 208 | 209 | Note that this command will generate a file config.php file for each domain under which the command has been executed. 210 | I.e. the command 211 | ``` 212 | php artisan config:cache --domain=site2.com 213 | ``` 214 | will generate the file 215 | ``` 216 | config-site2_com.php 217 | ``` 218 | 219 | ### Further information 220 | At run-time, the current HTTP domain is maintained in the laravel container 221 | and can be accessed by its `domain()` method added by this package. 222 | 223 | A `domainList()` method is available. It returns an associative array 224 | containing the installed domains info, similar to the `domain.list` command above. 225 | 226 | E.g. 227 | ``` 228 | [ 229 | site1.com => [ 230 | 'storage_path' => /site1_com, 231 | 'env' => '.env.site1.com' 232 | ] 233 | ] 234 | ``` 235 | 236 | #### Distinguishing between HTTP domains in web pages 237 | 238 | For each HTTP request received by the application, the specific environment file is 239 | loaded and the specific storage folder is used. 240 | 241 | If no specific environment file and/or storage folder is found, the standard one is used. 242 | 243 | The detection of the right HTTP domain is done by using the `$_SERVER['SERVER_NAME']` 244 | PHP variable. 245 | 246 | IMPORTANT NOTE: in some execution environments $_SERVER['SERVER_NAME'] is not instantiated, so 247 | this package does not work properly until you customize the detection of HTTP domains 248 | as described below. 249 | 250 | #### Customizing the detection of HTTP domains 251 | 252 | Starting from release 1.1.15, the detection of HTTP domains can be customized passing a `Closure` 253 | as the `domain_detection_function_web` entry of the new `domainParams` argument of `Application`'s 254 | constructor. In the following example, the HTTP domain detection relies upon `$_SERVER['HTTP_HOST']` 255 | instead of `$_SERVER['SERVER_NAME']`. 256 | 257 | ```php 258 | //use Illuminate\Foundation\Application; 259 | use Gecche\Multidomain\Foundation\Application; 260 | use Illuminate\Foundation\Configuration\Exceptions; 261 | use Illuminate\Foundation\Configuration\Middleware; 262 | 263 | $environmentPath = null; 264 | 265 | $domainParams = [ 266 | 'domain_detection_function_web' => function() { 267 | return \Illuminate\Support\Arr::get($_SERVER,'HTTP_HOST'); 268 | } 269 | ]; 270 | 271 | return Application::configure(basePath: dirname(__DIR__), 272 | environmentPath: $environmentPath, 273 | domainParams: $domainParams) 274 | ->withRouting( 275 | web: __DIR__.'/../routes/web.php', 276 | commands: __DIR__.'/../routes/console.php', 277 | health: '/up', 278 | ) 279 | ->withMiddleware(function (Middleware $middleware) { 280 | // 281 | }) 282 | ->withExceptions(function (Exceptions $exceptions) { 283 | // 284 | })->create(); 285 | ``` 286 | 287 | 288 | #### Using multi domains in artisan commands 289 | 290 | In order to distinguishing between domains, each artisan command accepts a new option: `domain`. E.g.: 291 | 292 | ``` 293 | php artisan list --domain=site1.com 294 | ``` 295 | 296 | The command will use the corresponding domain settings. 297 | 298 | #### About queues 299 | 300 | The artisan commands `queue:work` and `queue:listen` commands have been updated 301 | to accept a new `domain` option. 302 | ``` 303 | php artisan queue:work --domain=site1.com 304 | ``` 305 | As usual, the above command will use the corresponding domain settings. 306 | 307 | Keep in mind that if, for example, you are using the `database` driver and you have two domains sharing the same db, 308 | you should use two distinct queues if you want to manage the jobs of each domain separately. 309 | 310 | For example, you could: 311 | - put in your .env files a default queue for each domain, e.g. 312 | `QUEUE_DEFAULT=default1` for site1.com and `QUEUE_DEFAULT=default2` for site2.com 313 | - update the `queue.php` config file by changing the default queue accordingly: 314 | ``` 315 | 'database' => [ 316 | 'driver' => 'database', 317 | 'table' => 'jobs', 318 | 'queue' => env('QUEUE_DEFAULT','default'), 319 | 'retry_after' => 90, 320 | ], 321 | ``` 322 | 323 | - launch two distinct workers 324 | ``` 325 | php artisan queue:work --domain=site1.com --queue=default1 326 | ``` 327 | and 328 | ``` 329 | php artisan queue:work --domain=site1.com --queue=default2 330 | ``` 331 | 332 | Obviously, the same can be done for each other queue driver, apart from the `sync` driver. 333 | 334 | #### `storage:link` command 335 | 336 | If you make use of the `storage:link` command and you want a distinct symbolic link for each domain, you have to create 337 | them manually because to date such command always creates a link named `storage` and that name is hard coded in the 338 | command. Extending the `storage:link` command allowing to choose the name is outside the scope of this package 339 | (and I hope it will be done directly in future versions of Laravel). 340 | 341 | A way to obtain multiple storage links could be the following. 342 | Let us suppose to have two domains, namely `site1.com` and `site2.com` with associated storage folders 343 | `storage/site1_com` and `storage/site2_com`. 344 | 345 | 1. We manually create links for each domain: 346 | 347 | ``` 348 | ln -s storage/site1_com/app/public public/storage-site1_com 349 | ln -s storage/site2_com/app/public public/storage-site2_com 350 | ``` 351 | 352 | 2. In `.env.site1.com` and `.env.site2.com` we add an entry, e.g., for the first domain: 353 | 354 | ``` 355 | APP_PUBLIC_STORAGE=-site1_com 356 | ``` 357 | 358 | 3. In the `filesystems.php` config file we change as follows: 359 | 360 | ``` 361 | 'public' => [ 362 | 'driver' => 'local', 363 | 'root' => storage_path('app/public'), 364 | 'url' => env('APP_URL').'/storage'.env('APP_PUBLIC_STORAGE'), 365 | 'visibility' => 'public', 366 | ], 367 | ``` 368 | 369 | Furthermore, if you are using the package in a Single Page Application (SPA) setting, you could better handling distinct 370 | public resources for each domain via .htaccess or similar solutions as pointed out by [Scaenicus](https://github.com/Scaenicus) in his 371 | [.htaccess solution](https://github.com/gecche/laravel-multidomain/issues/11#issuecomment-559822284). 372 | 373 | #### Storing environment files in a custom folder 374 | 375 | Starting from version 1.1.11 a second argument has been added to the Application constructor in order to choose the 376 | folder where to place the environment files: if you have tens of domains, it is not very pleasant to have environment 377 | files in the root Laravel's app folder. 378 | 379 | So, if you want to use a different folder simply add it at the very top of the `bootstrap/app.php` file. for example, 380 | if you want to add environment files to the `envs` subfolder, simply do: 381 | 382 | ```php 383 | //use Illuminate\Foundation\Application; 384 | use Gecche\Multidomain\Foundation\Application; 385 | use Illuminate\Foundation\Configuration\Exceptions; 386 | use Illuminate\Foundation\Configuration\Middleware; 387 | 388 | $environmentPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'envs'; 389 | 390 | $domainParams = []; 391 | 392 | return Application::configure(basePath: dirname(__DIR__), 393 | environmentPath: $environmentPath, 394 | domainParams: $domainParams) 395 | ->withRouting( 396 | web: __DIR__.'/../routes/web.php', 397 | commands: __DIR__.'/../routes/console.php', 398 | health: '/up', 399 | ) 400 | ->withMiddleware(function (Middleware $middleware) { 401 | // 402 | }) 403 | ->withExceptions(function (Exceptions $exceptions) { 404 | // 405 | })->create(); 406 | ``` 407 | 408 | If you do not specify the second argument, the standard folder is assumed. Please note that if you specify a folder, 409 | also the standard `.env` file has to be placed in it 410 | 411 | #### Default environment files and storage folders 412 | 413 | If you try to run a web page or an shell command under a certain domain, e.g. `sub1.site1.com` and there is no specific 414 | environment file for that domain, i.e. the file `.env.sub1.site1.com` does not exist, the package will use the first 415 | available environment file by splitting the domain name with dots. In this example, the package searches for the 416 | the first environment file among the followings: 417 | 418 | ``` 419 | .env.site1.com 420 | .env.com 421 | .env 422 | ``` 423 | 424 | The same logic applies to the storage folder as well. 425 | 426 | #### About Laravel's Scheduler, Supervisor and some limitation 427 | 428 | If in your setting you make use of the Laravel's Scheduler, remember that also the command `schedule:run` has to be 429 | launched with the domain option. Hence, you have to launch a scheduler for each domain. 430 | At first one could think that one Scheduler instance should handle the commands launched for any domain, but the 431 | Scheduler itself is run within a Laravel Application, so the "env" under which it is run, automatically applies to 432 | each scheduled command and the `--domain` option has no effect at all. 433 | 434 | The same applies to externals tools like Supervisor: if you use Supervisor for artisan commands, e.g. the `queue:work` 435 | command, please be sure to prepare a command for each domain you want to handle. 436 | 437 | Due to the above, there are some cases in which the package can't work: in those settings where you don't have the 438 | possibility of changing for example the supervisor configuration rather than the `crontab` entries for the scheduler. 439 | Such an example has been pointed out [here](https://github.com/gecche/laravel-multidomain/issues/21#issuecomment-600469868) 440 | in which a Docker instance has been used. 441 | 442 | Lastly, be aware that some Laravel commands call other Artisan commands from the inside, 443 | obviously without the `--domain` option. The above situation does not work properly because the subcommand will work with 444 | the standard environment file. An example is the `migrate` command when using the `--seed` option. 445 | 446 | -------------------------------------------------------------------------------- /tests/work_in_progress/ArtisanTestCase.php: -------------------------------------------------------------------------------- 1 | files = new Filesystem(); 72 | $this->setPaths(); 73 | copy($this->laravelAppPath.'/config/app.php',$this->laravelAppPath.'/config/appORIG.php'); 74 | copy(__DIR__ . '/../config/app.php',$this->laravelAppPath.'/config/app.php'); 75 | copy($this->laravelAppPath.'/config/queue.php',$this->laravelAppPath.'/config/queueORIG.php'); 76 | copy(__DIR__ . '/../config/queue.php',$this->laravelAppPath.'/config/queue.php'); 77 | if (!is_dir($this->laravelEnvPath)) { 78 | mkdir($this->laravelEnvPath); 79 | } 80 | copy(__DIR__ . '/../.env.example',$this->laravelEnvPath.'/.env'); 81 | copy(__DIR__ . '/../'.$this->laravelArtisanFile,$this->laravelAppPath.DIRECTORY_SEPARATOR.'artisan'); 82 | 83 | foreach ($this->files->allFiles(__DIR__ . '/../database/migrations') as $file) { 84 | $relativeFile = substr($file,strrpos($file,'/')); 85 | copy($file,$this->laravelAppPath.'/database/migrations/'.$relativeFile); 86 | } 87 | 88 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'vendor:publish', '--provider="Gecche\Multidomain\Foundation\Providers\DomainConsoleServiceProvider"']); 89 | $process->run(); 90 | 91 | parent::setUp(); 92 | 93 | } 94 | 95 | protected function setPaths() { 96 | $this->laravelAppPath = __DIR__ . '/../../vendor/orchestra/testbench-core/laravel'; 97 | $this->laravelEnvPath = $this->laravelAppPath; 98 | } 99 | 100 | protected function tearDown(): void { 101 | 102 | $this->files->delete($this->laravelEnvPath.'/.env'); 103 | copy($this->laravelAppPath.'/config/appORIG.php',$this->laravelAppPath.'/config/app.php'); 104 | $this->files->delete($this->laravelAppPath.'/config/appORIG.php'); 105 | copy($this->laravelAppPath.'/config/queueORIG.php',$this->laravelAppPath.'/config/queue.php'); 106 | $this->files->delete($this->laravelAppPath.'/config/queueORIG.php'); 107 | 108 | } 109 | 110 | /** 111 | * Define environment setup. 112 | * 113 | * @param \Illuminate\Foundation\Application $app 114 | * @return void 115 | */ 116 | protected function getEnvironmentSetUp($app) 117 | { 118 | 119 | } 120 | 121 | 122 | /* 123 | * TEST FOR A BASIC COMMAND 124 | * We created a very simple "name" command (Gecche\Multidomain\Tests\Console\Commands\NameCommand) which simply displays the 125 | * text in the APP_NAME environment variable. 126 | * The default .env file containts APP_NAME=Laravel 127 | * First we create a new domain site1.test and we update his .env with APP_NAME=LARAVELTEST 128 | * Then we launch the "name" command twice: without options and with the --domain=site1.test option. 129 | * We expect to see "Laravel" andd then "LARAVELTEST" accordingly. 130 | */ 131 | public function testBasicCommand() { 132 | 133 | //Note that if the $_SERVER['SERVER_NAME'] value has been set and the --domain option has NOT been set, 134 | //the $_SERVER['SERVER_NAME'] value acts as the --domain option value. 135 | $serverName = Arr::get($_SERVER,'SERVER_NAME',''); 136 | 137 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:add', 'site1.test']); 138 | $process->run(); 139 | 140 | $domainValues = ['APP_NAME'=>'LARAVELTEST']; 141 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:update_env', 'site1.test', '--domain_values='.json_encode($domainValues)]); 142 | $process->run(); 143 | 144 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'name']); 145 | $process->run(); 146 | 147 | if (in_array($serverName, ['site1.test']) || Str::endsWith($serverName,'.site1.test')) { 148 | $this->assertEquals("LARAVELTEST",$process->getOutput()); 149 | } else { 150 | $this->assertEquals("Laravel",$process->getOutput()); 151 | } 152 | 153 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'key:generate', '--domain=site1.test']); 154 | $process->run(); 155 | 156 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'name', '--domain=site1.test']); 157 | $process->run(); 158 | $this->assertEquals("LARAVELTEST",$process->getOutput()); 159 | 160 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:remove', 'site1.test', '--force']); 161 | $process->run(); 162 | 163 | 164 | return; 165 | 166 | } 167 | 168 | /* 169 | * TEST FOR key:generate COMMAND 170 | */ 171 | public function testKeyGenerateCommand() { 172 | 173 | //Note that if the $_SERVER['SERVER_NAME'] value has been set and the --domain option has NOT been set, 174 | //the $_SERVER['SERVER_NAME'] value acts as the --domain option value. 175 | $serverName = Arr::get($_SERVER,'SERVER_NAME',''); 176 | 177 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:add', 'site1.test']); 178 | $process->run(); 179 | 180 | $domainValues = ['APP_NAME'=>'LARAVELTEST']; 181 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:update_env', 'site1.test', '--domain_values='.json_encode($domainValues)]); 182 | $process->run(); 183 | 184 | $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'key:generate']); 185 | $process->run(); 186 | $keyGenerateOutput = substr($process->getOutput(),17); 187 | $keyGenerateOutput = substr($keyGenerateOutput,0,strrpos($keyGenerateOutput,']')); 188 | 189 | 190 | if (in_array($serverName, ['site1.test']) || Str::endsWith($serverName,'.site1.test')) { 191 | $envFile = '.env.site1.test'; 192 | } else { 193 | $envFile = '.env'; 194 | } 195 | 196 | $this->assertStringContainsString('APP_KEY='.$keyGenerateOutput,$this->files->get($this->laravelEnvPath.DIRECTORY_SEPARATOR.$envFile)); 197 | 198 | return; 199 | 200 | } 201 | 202 | /* 203 | * TEST FOR QUEUE COMMANDS 204 | * In this test we check that queues with the database driver refer to the correct db depending upon the domain. 205 | * We use for simplicity the queue:flush command. 206 | * The default .env file containts QUEUE_DRIVER=database and DB_DATABASE=homestead 207 | * First we create a new domain site1.test and we update his .env with DB_DATABASE=site1 and we reset and run the migrations 208 | * in the homestead db. 209 | * Then, we check that the queue:flush command launched without options displays the standard success message 210 | * "All failed jobs deleted successfully!" and that the queue:flush command launched with the --domain=site1.test 211 | * option fails because it doesn't find the failed_job table as we did'nt migrate for that db. 212 | * Last, we repeat the check with the other db. 213 | * 214 | */ 215 | // public function testQueueCommand() { 216 | // //Note that if the $_SERVER['SERVER_NAME'] value has been set and the --domain option has NOT been set, 217 | // //the $_SERVER['SERVER_NAME'] value acts as the --domain option value. 218 | // // So all the artisan commands run as if the option were instantiated. 219 | // $serverName = Arr::get($_SERVER,'SERVER_NAME',''); 220 | // 221 | // 222 | // //ADDING DOMAIN AND UPDATING ENV 223 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:add', 'site1.test']); 224 | // $process->run(); 225 | // 226 | // $domainValues = ['DB_DATABASE'=>'site1']; 227 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:update_env', 'site1.test', '--domain_values='.json_encode($domainValues)]); 228 | // $process->run(); 229 | // 230 | // 231 | // //RESET MIGRATIONS IN BOTH DBS 232 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate:reset']); 233 | // $process->run(); 234 | // 235 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate:reset', '--domain=site1.test']); 236 | // $process->run(); 237 | // 238 | // //MIGRATIONS WITHOUT DOMAIN OPTION 239 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate']); 240 | // $process->run(); 241 | //// $this->assertEquals("Laravel",$process->getOutput()); 242 | // 243 | // 244 | // //CHECK QUEUE:FLUSH COMMAND: SUCCESS WITHOUT OPTIONS AND FAILURE WITH DOMAIN OPTION 245 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'queue:flush']); 246 | // $process->run(); 247 | // $this->assertStringContainsString('All failed jobs deleted successfully.',$process->getOutput()); 248 | // 249 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'queue:flush', '--domain=site1.test']); 250 | // $process->run(); 251 | // if (in_array($serverName, ['site1.test']) || Str::endsWith($serverName,'.site1.test')) { 252 | // $this->assertStringContainsString('All failed jobs deleted successfully.',$process->getOutput()); 253 | // } else { 254 | // $this->assertStringContainsString('SQLSTATE[42S02]: Base table or view not found: 1146 Table \'site1.failed',$process->getOutput()); 255 | // } 256 | // 257 | // 258 | // 259 | // //RESET MIGRATIONS IN BOTH DBS 260 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate:reset']); 261 | // $process->run(); 262 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate:reset', '--domain=site1.test']); 263 | // $process->run(); 264 | // 265 | // //MIGRATIONS WITH DOMAIN OPTION 266 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate', '--domain=site1.test']); 267 | // $process->run(); 268 | //// $this->assertEquals("Laravel",$process->getOutput()); 269 | // 270 | // //CHECK QUEUE:FLUSH COMMAND: SUCCESS WITH DOMAIN OPTIION AND FAILURE WITHOUT 271 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'queue:flush', '--domain=site1.test']); 272 | // $process->run(); 273 | // $this->assertStringContainsString('All failed jobs deleted successfully.',$process->getOutput()); 274 | // 275 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'queue:flush']); 276 | // $process->run(); 277 | // if (in_array($serverName, ['site1.test']) || Str::endsWith($serverName,'.site1.test')) { 278 | // $this->assertStringContainsString('All failed jobs deleted successfully.',$process->getOutput()); 279 | // } else { 280 | // $this->assertStringContainsString('SQLSTATE[42S02]: Base table or view not found: 1146 Table \'homestead.failed',$process->getOutput()); 281 | // } 282 | // 283 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:remove', 'site1.test', '--force']); 284 | // $process->run(); 285 | // 286 | // return; 287 | // 288 | // } 289 | 290 | /* 291 | * TEST FOR QUEUE LISTEN COMMAND 292 | * In this test we check that queues listeners applied to distinct domains work with the jobs pushed from 293 | * the corresponding domain and that the queues where they are pushed are those configured in the .env files. 294 | * 295 | * The job Gecche\Multidomain\Tests\Jobs\AppNameJob simply put in a file the name of the queue and the env(APP_NAME) value. 296 | * We push that job using an artisan Gecche\Multidomain\Tests\Console\Commands\QueuePush (queue_push) command which simply push the job in the queue. 297 | * 298 | */ 299 | // public function testQueueListenCommand() { 300 | // 301 | // 302 | // //Note that if the $_SERVER['SERVER_NAME'] value has been set and the --domain option has NOT been set, 303 | // //the $_SERVER['SERVER_NAME'] value acts as the --domain option value. 304 | // // So all the artisan commands run as if the option were instantiated. 305 | // $serverName = Arr::get($_SERVER,'SERVER_NAME',''); 306 | // 307 | // 308 | // //ADDING DOMAIN AND UPDATING ENV 309 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:add', 'site1.test']); 310 | // $process->run(); 311 | // 312 | // $domainValues = [ 313 | // 'APP_NAME' => 'LARAVELTEST', 314 | // 'DB_DATABASE'=>'site1', 315 | // 'QUEUE_DEFAULT' => 'site1' 316 | // ]; 317 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:update_env', 'site1.test', '--domain_values='.json_encode($domainValues)]); 318 | // $process->run(); 319 | // 320 | // 321 | // //RESET MIGRATIONS IN BOTH DBS 322 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate:reset']); 323 | // $process->run(); 324 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate:reset', '--domain=site1.test']); 325 | // $process->run(); 326 | // 327 | // //MIGRATIONS WITHOUT DOMAIN OPTION 328 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate']); 329 | // $process->run(); 330 | // 331 | // 332 | // //This the file used by the AppNameJob job 333 | // $fileToTest = $this->laravelAppPath.'/queueresult.txt'; 334 | // 335 | // $this->files->delete($fileToTest); 336 | // 337 | // $this->assertFileDoesNotExist($fileToTest); 338 | // 339 | // //We start the listener 340 | // $processName = "php ".$this->laravelAppPath. "/artisan queue:listen"; 341 | // $process = new Process(explode(" ",$processName)); 342 | // $process->start(); 343 | // 344 | // //We push the Job using the queue_push command 345 | // $process2 = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'queue_push']); 346 | // $process2->run(); 347 | // 348 | // //Wait for a reasonable time (the only way we managed to simulate queue listeners) 349 | // sleep(5); 350 | // 351 | // //We assert that the file exists 352 | // $this->assertFileExists($fileToTest); 353 | // 354 | // //Depending upon the domain option (or the SERVER_NAME value) 355 | // //we check accordingly the contents of the file 356 | // $fileContent = $this->files->get($fileToTest); 357 | // if (in_array($serverName, ['site1.test']) || Str::endsWith($serverName,'.site1.test')) { 358 | // $this->assertStringContainsString('LARAVELTEST --- site1',$fileContent); 359 | // } else { 360 | // $this->assertStringContainsString('Laravel --- default',$fileContent); 361 | // } 362 | // 363 | // 364 | // $process->stop(0); 365 | // 366 | // //We kill the listener process by name 367 | // $string = 'pkill -f "' . $processName . '"'; 368 | // exec($string); 369 | // 370 | // 371 | // /* 372 | // * We repeat the stuff under the domain "site1.test" 373 | // */ 374 | // /***/ 375 | // //MIGRATIONS WITH DOMAIN OPTION 376 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'migrate', '--domain=site1.test']); 377 | // $process->run(); 378 | // 379 | // $this->files->delete($fileToTest); 380 | // 381 | // $this->assertFileDoesNotExist($fileToTest); 382 | // 383 | // $processName = 'php '.$this->laravelAppPath.'/artisan queue:listen --domain=site1.test'; 384 | // $process = new Process(explode(" ",$processName)); 385 | // $process->start(); 386 | // 387 | // $process2 = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'queue_push', '--domain=site1.test']); 388 | // $process2->run(); 389 | // 390 | // //Wait for a reasonable time 391 | // sleep(5); 392 | // 393 | // $this->assertFileExists($fileToTest); 394 | // 395 | // $fileContent = $this->files->get($fileToTest); 396 | // $this->assertStringContainsString('LARAVELTEST --- site1',$fileContent); 397 | // 398 | // $process->stop(0); 399 | // 400 | // $string = 'pkill -f "' . $processName . '"'; 401 | // exec($string); 402 | // 403 | // $process = new Process([$this->phpProcess, $this->laravelAppPath.'/artisan', 'domain:remove', 'site1.test', '--force']); 404 | // $process->run(); 405 | // 406 | // 407 | // $this->files->delete($fileToTest); 408 | // 409 | // return; 410 | // 411 | // } 412 | } 413 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | 6 | ## [Unreleased] 7 | 8 | ## 12.1 - 2025-04-15 9 | ### Changed 10 | - Bugfix (cache correctly included in trait) 11 | 12 | ## 12.0 - 2024-02-26 13 | ### Changed 14 | - `composer.json` updated for Laravel 12.x 15 | - Updated tests for PHPUnit 11 16 | 17 | ## 11.2 - 2025-04-15 18 | ### Changed 19 | - Bugfix (cache correctly included in trait) 20 | 21 | ## 11.1 - 2024-05-30 22 | ### Changed 23 | - Bugfix for discovering commands 24 | - Bugfix for passing environmentPath and domainParams to the application constructor 25 | - Updated README.md 26 | 27 | ## 11.0 - 2024-03-19 28 | ### Changed 29 | - `composer.json` updated for Laravel 11.x 30 | - Updated README.md for new streamlined installation instructions 31 | 32 | ## 10.2 - 2024-02-22 33 | ### Changed 34 | - Refixed a problem in the `queue:listen` command :) 35 | 36 | ## 10.1 - 2024-02-18 37 | ### Changed 38 | - Fixed a problem in the `queue:listen` command 39 | 40 | ## 10.0 - 2023-02-20 41 | ### Changed 42 | - Integrated [PR #72](https://github.com/gecche/laravel-multidomain/pull/72) for adding more output options to the 43 | `domain:list` command 44 | - Integrated [PR #88](https://github.com/gecche/laravel-multidomain/pull/88) for better handling git files in local 45 | development 46 | - Version numbering following Laravel versions from 10.x 47 | - Updated some methods to adapt to Laravel 10.x 48 | - `composer.json` updated for Laravel 10.x 49 | 50 | ## 5.0 - 2022-02-08 51 | ### Changed 52 | - Updated some core methods to adapt to Laravel 9.x changes 53 | - `composer.json` updated for Laravel 9.x 54 | 55 | ## 4.2.1 - 2022-02-04 56 | ### Changed 57 | - Improved PHP 8.1 compatibility, see [PR #68](https://github.com/gecche/laravel-multidomain/pull/68) 58 | - README.md updated for misleading `update_env` command's instructions 59 | 60 | ## 4.2 - 2021-02-13 61 | ### Added 62 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 63 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 64 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 65 | ### Changed 66 | - README.md updated 67 | 68 | ## 4.1 - 2020-11-17 69 | ### Changed 70 | - Updated `queue:listen` command adapting it to the new Laravel 8.x signature 71 | - Compatibility with Laravel Horizon 72 | 73 | ## 4.0 - 2020-10-07 74 | ### Changed 75 | - `composer.json` updated for Laravel 8.x 76 | 77 | ## 3.9 - 2021-02-13 78 | ### Added 79 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 80 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 81 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 82 | ### Changed 83 | - README.md updated 84 | 85 | ## 3.8 - 2020-10-07 86 | ### Changed 87 | - New logic for default environment files and storage folders 88 | - Tests improved and updated 89 | - README.md updated 90 | 91 | ## 3.7 - 2020-05-24 92 | ### Changed 93 | - README.md updated with some minor correction and frontend image. (I've done a mistake with tag names: 94 | from 3.2 to 3.7 instead of 3.3 :) ) 95 | 96 | ## 3.2 - 2020-04-23 97 | ### Changed 98 | - Improved robustness in `environmentFileDomain` when no domain is given. 99 | 100 | ## 3.1 - 2020-04-19 101 | ### Added 102 | - An optional argument to the Application constructor for handling the storage of env files in a different folder 103 | than the root Laravel's app folder. With new tests. 104 | - The `env_path` helper to get the folder in which env files are placed. 105 | ### Changed 106 | - README.md updated 107 | 108 | ## 3.0 - 2020-03-09 109 | ### Changed 110 | - `composer.json` updated for Laravel 7.x 111 | 112 | ## 2.9 - 2021-02-13 113 | ### Added 114 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 115 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 116 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 117 | ### Changed 118 | - README.md updated 119 | 120 | ## 2.8 - 2020-10-07 121 | ### Changed 122 | - New logic for default environment files and storage folders 123 | - Tests improved and updated 124 | - README.md updated 125 | 126 | ## 2.7 - 2020-05-24 127 | ### Changed 128 | - README.md updated with some minor correction and frontend image. 129 | 130 | ## 2.6 - 2020-04-23 131 | ### Changed 132 | - Improved robustness in `environmentFileDomain` when no domain is given. 133 | 134 | ## 2.5 - 2020-04-19 135 | ### Added 136 | - An optional argument to the Application constructor for handling the storage of env files in a different folder 137 | than the root Laravel's app folder. With new tests. 138 | - The `env_path` helper to get the folder in which env files are placed. 139 | ### Changed 140 | - README.md updated 141 | 142 | ## 2.4 - 2020-03-09 143 | ### Changed 144 | - Force the detection of the domain when trying to access to domain info if the domain has never been detected 145 | - Improved tests 146 | 147 | ## 2.3 - 2020-01-17 148 | ### Changed 149 | - README.md: added notes on `storage:link` command 150 | 151 | ## 2.2 - 2019-11-15 152 | ### Changed 153 | - Changed `Gecche\Multidomain\Foundation\Application` for handling separated cache files for each 154 | domain when using the `route:cache` or `event:cache` Laravel commands 155 | 156 | ## 2.1 - 2019-11-12 157 | ### Changed 158 | - Bugfix in `Gecche\Multidomain\Foundation\Console\DomainCommandTrait`: running the 159 | `domain:add` command, sometimes some lines in the stub .env file were 160 | skipped and not replicated in the new .env. file 161 | 162 | ## 2.0 - 2019-10-29 163 | ### Changed 164 | - `composer.json` updated for Laravel 6.x 165 | 166 | ## 1.4.9 - 2021-02-13 167 | ### Added 168 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 169 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 170 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 171 | ### Changed 172 | - README.md updated 173 | 174 | ## 1.4.8 - 2020-10-07 175 | ### Changed 176 | - New logic for default environment files and storage folders 177 | - Tests improved and updated 178 | - README.md updated 179 | 180 | ## 1.4.7 - 2020-05-24 181 | ### Changed 182 | - README.md updated with some minor correction and frontend image. 183 | 184 | ## 1.4.6 - 2020-04-23 185 | ### Changed 186 | - Improved robustness in `environmentFileDomain` when no domain is given. 187 | 188 | ## 1.4.5 - 2020-04-19 189 | ### Added 190 | - An optional argument to the Application constructor for handling the storage of env files in a different folder 191 | than the root Laravel's app folder. With new tests. 192 | - The `env_path` helper to get the folder in which env files are placed. 193 | ### Changed 194 | - README.md updated 195 | 196 | ## 1.4.4 - 2020-03-09 197 | ### Changed 198 | - Force the detection of the domain when trying to access to domain info if the domain has never been detected 199 | - Improved tests 200 | 201 | ## 1.4.3 - 2020-01-17 202 | ### Changed 203 | - README.md: added notes on `storage:link` command 204 | 205 | ## 1.4.2 - 2019-11-15 206 | ### Changed 207 | - Changed `Gecche\Multidomain\Foundation\Application` for handling separated cache files for each 208 | domain when using the `route:cache` or `event:cache` Laravel commands 209 | 210 | ## 1.4.1 - 2019-11-12 211 | ### Changed 212 | - Bugfix in `Gecche\Multidomain\Foundation\Console\DomainCommandTrait`: running the 213 | `domain:add` command, sometimes some lines in the stub .env file were 214 | skipped and not replicated in the new .env. file 215 | 216 | ## 1.4.0 - 2019-10-29 217 | ### Changed 218 | - `composer.json` updated for Laravel 5.8 219 | 220 | ## 1.3.9 - 2021-02-13 221 | ### Added 222 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 223 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 224 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 225 | ### Changed 226 | - README.md updated 227 | 228 | ## 1.3.8 - 2020-10-07 229 | ### Changed 230 | - New logic for default environment files and storage folders 231 | - Tests improved and updated 232 | - README.md updated 233 | 234 | ## 1.3.7 - 2020-05-24 235 | ### Changed 236 | - README.md updated with some minor correction and frontend image. 237 | 238 | ## 1.3.6 - 2020-04-23 239 | ### Changed 240 | - Improved robustness in `environmentFileDomain` when no domain is given. 241 | 242 | ## 1.3.5 - 2020-04-19 243 | ### Added 244 | - An optional argument to the Application constructor for handling the storage of env files in a different folder 245 | than the root Laravel's app folder. With new tests. 246 | - The `env_path` helper to get the folder in which env files are placed. 247 | ### Changed 248 | - README.md updated 249 | 250 | ## 1.3.4 - 2020-03-09 251 | ### Changed 252 | - Force the detection of the domain when trying to access to domain info if the domain has never been detected 253 | - Improved tests 254 | 255 | ## 1.3.3 - 2020-01-17 256 | ### Changed 257 | - README.md: added notes on `storage:link` command 258 | 259 | ## 1.3.2 - 2019-11-15 260 | ### Changed 261 | - Changed `Gecche\Multidomain\Foundation\Application` for handling separated cache files for each 262 | domain when using the `route:cache` Laravel command 263 | 264 | ## 1.3.1 - 2019-11-12 265 | ### Changed 266 | - Bugfix in `Gecche\Multidomain\Foundation\Console\DomainCommandTrait`: running the 267 | `domain:add` command, sometimes some lines in the stub .env file were 268 | skipped and not replicated in the new .env. file 269 | 270 | ## 1.3.0 - 2019-10-29 271 | ### Changed 272 | - `composer.json` updated for Laravel 5.7 273 | - Bugfix in `Gecche\Multidomain\Queue\Listener` due to changes in handling 274 | worker commands in the parent class. 275 | 276 | ## 1.2.9 - 2021-02-13 277 | ### Added 278 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 279 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 280 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 281 | ### Changed 282 | - README.md updated 283 | 284 | ## 1.2.8 - 2020-10-06 285 | ### Changed 286 | - New logic for default environment files and storage folders 287 | - Tests improved and updated 288 | - README.md updated 289 | 290 | ## 1.2.7 - 2020-05-24 291 | ### Changed 292 | - README.md updated with some minor correction and frontend image. 293 | 294 | ## 1.2.6 - 2020-04-23 295 | ### Changed 296 | - Improved robustness in `environmentFileDomain` when no domain is given. 297 | 298 | ## 1.2.5 - 2020-04-19 299 | ### Added 300 | - An optional argument to the Application constructor for handling the storage of env files in a different folder 301 | than the root Laravel's app folder. With new tests. 302 | - The `env_path` helper to get the folder in which env files are placed. 303 | ### Changed 304 | - README.md updated 305 | 306 | ## 1.2.4 - 2020-03-09 307 | ### Changed 308 | - Force the detection of the domain when trying to access to domain info if the domain has never been detected 309 | - Improved tests 310 | 311 | ## 1.2.3 - 2020-01-17 312 | ### Changed 313 | - README.md: added notes on `storage:link` command 314 | 315 | ## 1.2.2 - 2019-11-15 316 | ### Changed 317 | - Changed `Gecche\Multidomain\Foundation\Application` for handling separated cache files for each 318 | domain when using the `route:cache` Laravel command 319 | 320 | ## 1.2.1 - 2019-11-12 321 | ### Changed 322 | - Bugfix in `Gecche\Multidomain\Foundation\Console\DomainCommandTrait`: running the 323 | `domain:add` command, sometimes some lines in the stub .env file were 324 | skipped and not replicated in the new .env. file 325 | 326 | ## 1.2.0 - 2019-10-29 327 | ### Changed 328 | - `composer.json` updated for Laravel 5.6 329 | 330 | ## 1.1.15 - 2021-02-13 331 | ### Added 332 | - New `Application`'s constructor param `domainParams` for storing miscellaneous params used for handling domains: 333 | the first one is the `domain_detection_function_web` param which accept a `Closure` for customizing 334 | the doamin detection function instead of relying upon `$_SERVER['SERVER_NAME']` value 335 | ### Changed 336 | - README.md updated 337 | 338 | ## 1.1.14 - 2020-10-06 339 | ### Changed 340 | - New logic for default environment files and storage folders 341 | - Tests improved and updated 342 | - README.md updated 343 | 344 | ## 1.1.13 - 2020-05-24 345 | ### Changed 346 | - README.md updated with some minor correction and frontend image. 347 | 348 | ## 1.1.12 - 2020-04-23 349 | ### Changed 350 | - Improved robustness in `environmentFileDomain` when no domain is given. 351 | 352 | ## 1.1.11 - 2020-04-16 353 | ### Added 354 | - An optional argument to the Application constructor for handling the storage of env files in a different folder 355 | than the root Laravel's app folder. With new tests. 356 | - The `env_path` helper to get the folder in which env files are placed. 357 | ### Changed 358 | - README.md updated 359 | 360 | ## 1.1.10 - 2020-03-09 361 | ### Changed 362 | - Force the detection of the domain when trying to access to domain info if the domain has never been detected 363 | - Improved tests 364 | 365 | ## 1.1.9 - 2020-01-17 366 | ### Changed 367 | - README.md: added notes on `storage:link` command 368 | 369 | ## 1.1.8 - 2019-11-15 370 | ### Changed 371 | - Changed `Gecche\Multidomain\Foundation\Application` for handling separated cache files for each 372 | domain when using the `route:cache` Laravel command 373 | 374 | ## 1.1.7 - 2019-11-12 375 | ### Changed 376 | - Bugfix in `Gecche\Multidomain\Foundation\Console\DomainCommandTrait`: running the 377 | `domain:add` command, sometimes some lines in the stub .env file were 378 | skipped and not replicated in the new .env. file 379 | 380 | ## 1.1.6 - 2019-10-29 381 | ### Added 382 | - Test suites 383 | ### Changed 384 | - Namespace of `DomainConsoleServiceProvider` provider from 385 | `Gecche\Multidomain\Foundation` to `Gecche\Multidomain\Foundation\Providers` 386 | in order to respect folder struture. 387 | - `composer.json` file for testing purposes (from now Git branches are separated for each 388 | Laravel release starting from 5.5 and as pointed out in the docs) 389 | 390 | ## 1.1.5 - 2019-09-19 391 | ### Changed 392 | - Compatibility with Laravel 6.x (note that previous version 1.1.4 is also compatible with Laravel 6.0.x including [laravel/helpers](https://github.com/laravel/helpers) in the composer.json. file of your Laravel Application) 393 | 394 | ## 1.1.4 - 2019-05-25 395 | ### Changed 396 | - Better English in README.md :) Thanks to leadegroot. 397 | 398 | ## 1.1.3 - 2019-05-22 399 | ### Changed 400 | - README.md improved with queue example 401 | 402 | ## 1.1.2 - 2019-03-23 403 | ### Added 404 | - Added new domain:list artisan command displaying info of installed domains 405 | - Added domainsList() method to Application 406 | 407 | ## 1.1.1 - 2019-03-10 408 | ### Added 409 | - This CHANGELOG.md file. 410 | - Added the handling of config:cache artisan command by multiple cache config files 411 | 412 | ## 1.1.0 - 2018-06-24 413 | ### Added 414 | - Initial version for Laravel 5.5. 415 | 416 | [Unreleased]: https://github.com/gecche/laravel-multidomain/compare/v12.1...HEAD 417 | [12.1]: https://github.com/gecche/laravel-multidomain/compare/v12.1...v12.0 418 | [12.0]: https://github.com/gecche/laravel-multidomain/compare/v12.0...v11.2 419 | [11.2]: https://github.com/gecche/laravel-multidomain/compare/v11.2...v11.1 420 | [11.1]: https://github.com/gecche/laravel-multidomain/compare/v11.0...v11.1 421 | [11.0]: https://github.com/gecche/laravel-multidomain/compare/v10.2...v11.0 422 | [10.2]: https://github.com/gecche/laravel-multidomain/compare/v10.1...v10.2 423 | [10.1]: https://github.com/gecche/laravel-multidomain/compare/v10.0...v10.1 424 | [10.0]: https://github.com/gecche/laravel-multidomain/compare/v5.0...v10.0 425 | [5.0]: https://github.com/gecche/laravel-multidomain/compare/v4.2.1...v5.0 426 | [4.2.1]: https://github.com/gecche/laravel-multidomain/compare/v4.2...v4.2.1 427 | [4.2]: https://github.com/gecche/laravel-multidomain/compare/v4.1...v4.2 428 | [4.1]: https://github.com/gecche/laravel-multidomain/compare/v4.0...v4.1 429 | [4.0]: https://github.com/gecche/laravel-multidomain/compare/v3.8...v4.0 430 | [3.9]: https://github.com/gecche/laravel-multidomain/compare/v3.8...v3.9 431 | [3.8]: https://github.com/gecche/laravel-multidomain/compare/v3.7...v3.8 432 | [3.7]: https://github.com/gecche/laravel-multidomain/compare/v3.2...v3.7 433 | [3.2]: https://github.com/gecche/laravel-multidomain/compare/v3.1...v3.2 434 | [3.1]: https://github.com/gecche/laravel-multidomain/compare/v3.0...v3.1 435 | [3.0]: https://github.com/gecche/laravel-multidomain/compare/v2.4...v3.0 436 | [2.9]: https://github.com/gecche/laravel-multidomain/compare/v2.8...v2.9 437 | [2.8]: https://github.com/gecche/laravel-multidomain/compare/v2.7...v2.8 438 | [2.7]: https://github.com/gecche/laravel-multidomain/compare/v2.6...v2.7 439 | [2.6]: https://github.com/gecche/laravel-multidomain/compare/v2.5...v2.6 440 | [2.5]: https://github.com/gecche/laravel-multidomain/compare/v2.4...v2.5 441 | [2.4]: https://github.com/gecche/laravel-multidomain/compare/v2.3...v2.4 442 | [2.3]: https://github.com/gecche/laravel-multidomain/compare/v2.2...v2.3 443 | [2.2]: https://github.com/gecche/laravel-multidomain/compare/v2.1...v2.2 444 | [2.1]: https://github.com/gecche/laravel-multidomain/compare/v2.0...v2.1 445 | [2.0]: https://github.com/gecche/laravel-multidomain/compare/v1.4.0...v2.0 446 | [1.4.9]: https://github.com/gecche/laravel-multidomain/compare/v1.4.8...v1.4.9 447 | [1.4.8]: https://github.com/gecche/laravel-multidomain/compare/v1.4.7...v1.4.8 448 | [1.4.7]: https://github.com/gecche/laravel-multidomain/compare/v1.4.6...v1.4.7 449 | [1.4.6]: https://github.com/gecche/laravel-multidomain/compare/v1.4.5...v1.4.6 450 | [1.4.5]: https://github.com/gecche/laravel-multidomain/compare/v1.4.4...v1.4.5 451 | [1.4.4]: https://github.com/gecche/laravel-multidomain/compare/v1.4.3...v1.4.4 452 | [1.4.3]: https://github.com/gecche/laravel-multidomain/compare/v1.4.2...v1.4.3 453 | [1.4.2]: https://github.com/gecche/laravel-multidomain/compare/v1.4.1...v1.4.2 454 | [1.4.1]: https://github.com/gecche/laravel-multidomain/compare/v1.4.0...v1.4.1 455 | [1.4.0]: https://github.com/gecche/laravel-multidomain/compare/v1.3.0...v1.4.0 456 | [1.3.9]: https://github.com/gecche/laravel-multidomain/compare/v1.3.8...v1.3.9 457 | [1.3.8]: https://github.com/gecche/laravel-multidomain/compare/v1.3.7...v1.3.8 458 | [1.3.7]: https://github.com/gecche/laravel-multidomain/compare/v1.3.6...v1.3.7 459 | [1.3.6]: https://github.com/gecche/laravel-multidomain/compare/v1.3.5...v1.3.6 460 | [1.3.5]: https://github.com/gecche/laravel-multidomain/compare/v1.3.4...v1.3.5 461 | [1.3.4]: https://github.com/gecche/laravel-multidomain/compare/v1.3.3...v1.3.4 462 | [1.3.3]: https://github.com/gecche/laravel-multidomain/compare/v1.3.2...v1.3.3 463 | [1.3.2]: https://github.com/gecche/laravel-multidomain/compare/v1.3.1...v1.3.2 464 | [1.3.1]: https://github.com/gecche/laravel-multidomain/compare/v1.3.0...v1.3.1 465 | [1.3.0]: https://github.com/gecche/laravel-multidomain/compare/v1.2.0...v1.3.0 466 | [1.2.9]: https://github.com/gecche/laravel-multidomain/compare/v1.2.8...v1.2.9 467 | [1.2.8]: https://github.com/gecche/laravel-multidomain/compare/v1.2.7...v1.2.8 468 | [1.2.7]: https://github.com/gecche/laravel-multidomain/compare/v1.2.6...v1.2.7 469 | [1.2.6]: https://github.com/gecche/laravel-multidomain/compare/v1.2.5...v1.2.6 470 | [1.2.5]: https://github.com/gecche/laravel-multidomain/compare/v1.2.4...v1.2.5 471 | [1.2.4]: https://github.com/gecche/laravel-multidomain/compare/v1.2.3...v1.2.4 472 | [1.2.3]: https://github.com/gecche/laravel-multidomain/compare/v1.2.2...v1.2.3 473 | [1.2.2]: https://github.com/gecche/laravel-multidomain/compare/v1.2.1...v1.2.2 474 | [1.2.1]: https://github.com/gecche/laravel-multidomain/compare/v1.2.0...v1.2.1 475 | [1.2.0]: https://github.com/gecche/laravel-multidomain/compare/v1.1.6...v1.2.0 476 | [1.1.15]: https://github.com/gecche/laravel-multidomain/compare/v1.1.14...v1.1.15 477 | [1.1.14]: https://github.com/gecche/laravel-multidomain/compare/v1.1.13...v1.1.14 478 | [1.1.13]: https://github.com/gecche/laravel-multidomain/compare/v1.1.12...v1.1.13 479 | [1.1.12]: https://github.com/gecche/laravel-multidomain/compare/v1.1.11...v1.1.12 480 | [1.1.11]: https://github.com/gecche/laravel-multidomain/compare/v1.1.10...v1.1.11 481 | [1.1.10]: https://github.com/gecche/laravel-multidomain/compare/v1.1.9...v1.1.10 482 | [1.1.9]: https://github.com/gecche/laravel-multidomain/compare/v1.1.8...v1.1.9 483 | [1.1.8]: https://github.com/gecche/laravel-multidomain/compare/v1.1.7...v1.1.8 484 | [1.1.7]: https://github.com/gecche/laravel-multidomain/compare/v1.1.6...v1.1.7 485 | [1.1.6]: https://github.com/gecche/laravel-multidomain/compare/v1.1.5...v1.1.6 486 | [1.1.5]: https://github.com/gecche/laravel-multidomain/compare/v1.1.4...v1.1.5 487 | [1.1.4]: https://github.com/gecche/laravel-multidomain/compare/v1.1.3...v1.1.4 488 | [1.1.3]: https://github.com/gecche/laravel-multidomain/compare/v1.1.2...v1.1.3 489 | [1.1.2]: https://github.com/gecche/laravel-multidomain/compare/v1.1.1...v1.1.2 490 | [1.1.1]: https://github.com/gecche/laravel-multidomain/compare/v1.1.0...v1.1.1 491 | 492 | [reference]: https://keepachangelog.com/en/1.0.0/ 493 | --------------------------------------------------------------------------------