Apply module migrations when the Modules folder is outside the app folder
module:migrate hands the module's migrations folder to the migrate command after removing base_path() from it. Module::getPath() is a realpath(), so on installations where the Modules folder is a symlink leading outside of the application folder (a common container layout: /var/www/html/Modules -> /data/Modules) base_path() does not occur in that path. The value stays absolute, migrate prepends base_path() to it anyway (BaseCommand:: getMigrationPaths()), the resulting folder does not exist, and the migration reports "Nothing to migrate" with exit code 0. Updating a module through the Modules page then leaves the old schema in place, because Module::updateModule() relies on freescout:module-install and nothing reports the failure. Activating a module is not affected, as ModulesController runs a plain migrate afterwards. Modules now remember the path they have been scanned from, which still contains the symlink and is therefore relative to base_path(), and the module:migrate command builds the --path option from it. The scanned path also travels through the modules cache, as Collection::toArray() keeps the resolved path only. When neither path is inside the application folder the command reports it instead of reporting success. closes #5569
This commit is contained in:
96
app/Console/Commands/ModuleMigrate.php
Normal file
96
app/Console/Commands/ModuleMigrate.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* php artisan module:migrate modulename.
|
||||
*
|
||||
* Replaces the command from nwidart/laravel-modules, which passes the module's
|
||||
* migrations folder to the migrate command after removing base_path() from it.
|
||||
* Module::getPath() is a realpath(), so on installations where the Modules
|
||||
* folder is a symlink leading outside of the application folder (a common
|
||||
* container layout: /var/www/html/Modules -> /data/Modules) base_path() does
|
||||
* not occur in that path. The value stays absolute, migrate prefixes
|
||||
* base_path() to it anyway (Illuminate\Database\Console\Migrations\
|
||||
* BaseCommand::getMigrationPaths()), the resulting folder does not exist and
|
||||
* the migration reports "Nothing to migrate" with a zero exit code.
|
||||
*
|
||||
* https://github.com/freescout-help-desk/freescout/issues/5569
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Nwidart\Modules\Commands\MigrateCommand;
|
||||
use Nwidart\Modules\Migrations\Migrator;
|
||||
use Nwidart\Modules\Module;
|
||||
|
||||
class ModuleMigrate extends MigrateCommand
|
||||
{
|
||||
/**
|
||||
* Run the migrations of the specified module.
|
||||
*
|
||||
* @param Module $module
|
||||
*/
|
||||
protected function migrate(Module $module)
|
||||
{
|
||||
$path = $this->getMigrationPath($module);
|
||||
|
||||
if ($this->option('subpath')) {
|
||||
$path = $path.'/'.$this->option('subpath');
|
||||
}
|
||||
|
||||
$this->call('migrate', [
|
||||
'--path' => $path,
|
||||
'--database' => $this->option('database'),
|
||||
'--pretend' => $this->option('pretend'),
|
||||
'--force' => $this->option('force'),
|
||||
]);
|
||||
|
||||
if ($this->option('seed')) {
|
||||
$this->call('module:seed', ['module' => $module->getName()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module's migrations folder relative to base_path(), as the
|
||||
* migrate command prepends base_path() to whatever --path receives.
|
||||
*
|
||||
* @param Module $module
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getMigrationPath(Module $module)
|
||||
{
|
||||
$absolute_path = (new Migrator($module))->getPath();
|
||||
|
||||
$path = str_replace(base_path(), '', $absolute_path);
|
||||
|
||||
if ($path != $absolute_path) {
|
||||
// The module is located inside of the application folder.
|
||||
return $path;
|
||||
}
|
||||
|
||||
// The module path is a realpath() leading outside of the application
|
||||
// folder. Rebuild the path from the folder the module has been scanned
|
||||
// from, which still contains the symlink and is therefore located
|
||||
// inside of the application folder.
|
||||
$module_path = $module->getPath();
|
||||
|
||||
if (method_exists($module, 'getScannedPath')
|
||||
&& $module_path
|
||||
&& strpos($absolute_path, $module_path) === 0
|
||||
) {
|
||||
$scanned_path = str_replace(base_path(), '', $module->getScannedPath());
|
||||
$scanned_path .= substr($absolute_path, strlen($module_path));
|
||||
|
||||
if (is_dir(base_path().DIRECTORY_SEPARATOR.$scanned_path)) {
|
||||
return $scanned_path;
|
||||
}
|
||||
}
|
||||
|
||||
// Neither path is located inside of the application folder, so the
|
||||
// migrate command can not resolve it. Tell the user instead of
|
||||
// reporting "Nothing to migrate".
|
||||
$this->error('Module migrations folder is located outside of the application folder: '.$absolute_path);
|
||||
$this->error('Run "php artisan migrate" to apply the module migrations.');
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,15 @@ abstract class Module extends ServiceProvider
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* The module path as it has been scanned, before realpath() resolved it.
|
||||
* When the Modules folder is a symlink leading outside of the application
|
||||
* folder, this is the only path which is still relative to base_path().
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $scanned_path;
|
||||
|
||||
/**
|
||||
* @var array of cached Json objects, keyed by filename
|
||||
*/
|
||||
@@ -49,6 +58,7 @@ abstract class Module extends ServiceProvider
|
||||
parent::__construct($app);
|
||||
$this->name = $name;
|
||||
$this->path = realpath($path);
|
||||
$this->scanned_path = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,6 +161,16 @@ abstract class Module extends ServiceProvider
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path the module has been scanned from, with symlinks not resolved.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScannedPath()
|
||||
{
|
||||
return $this->scanned_path ?: $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set path.
|
||||
*
|
||||
|
||||
@@ -202,7 +202,11 @@ abstract class Repository implements RepositoryInterface, Countable
|
||||
$modules = [];
|
||||
|
||||
foreach ($cached as $name => $module) {
|
||||
$path = $module['path'];
|
||||
// Prefer the path the module has been scanned from, with symlinks
|
||||
// not resolved yet - the constructor resolves it again, so getPath()
|
||||
// stays the same, but the module also knows its original location.
|
||||
// Modules cached before this was stored only have 'path'.
|
||||
$path = !empty($module['scanned_path']) ? $module['scanned_path'] : $module['path'];
|
||||
|
||||
$modules[$name] = $this->createModule($this->app, $name, $path);
|
||||
}
|
||||
@@ -223,13 +227,22 @@ abstract class Repository implements RepositoryInterface, Countable
|
||||
|
||||
return $this->app['cache']->remember($this->config('cache.key'), $this->config('cache.lifetime'), function () {
|
||||
|
||||
$modules = $this->scan();
|
||||
|
||||
// By some reason when Nwidart\Modules\Module is converted into array
|
||||
$array = $this->toCollection()->toArray();
|
||||
$array = (new Collection($modules))->toArray();
|
||||
// Set `active` flag from DB for each module
|
||||
foreach ($array as $key => $item) {
|
||||
if (!empty($item['alias'])) {
|
||||
$item['active'] = (int) \App\Module::isActive($item['alias']);
|
||||
}
|
||||
|
||||
// Collection::toArray() only keeps the resolved path, but the
|
||||
// path the module has been scanned from is needed to build
|
||||
// migration paths relative to base_path().
|
||||
if (isset($modules[$key]) && method_exists($modules[$key], 'getScannedPath')) {
|
||||
$array[$key]['scanned_path'] = $modules[$key]->getScannedPath();
|
||||
}
|
||||
}
|
||||
|
||||
// Remember in memory cache to avoid reading cache file
|
||||
|
||||
Reference in New Issue
Block a user