From cf14306c2b3f82a81f8d56669a71633b4d4b5fce Mon Sep 17 00:00:00 2001
From: marvin-borner@live.com
Date: Mon, 16 Apr 2018 21:09:05 +0200
Subject: Main merge to user management system - files are now at /main/public/
---
main/app/system/Bakery/Bakery.php | 166 ++++++
main/app/system/Bakery/BaseCommand.php | 58 ++
main/app/system/Bakery/Command/Bake.php | 77 +++
main/app/system/Bakery/Command/BuildAssets.php | 180 +++++++
main/app/system/Bakery/Command/ClearCache.php | 95 ++++
main/app/system/Bakery/Command/Debug.php | 185 +++++++
main/app/system/Bakery/Command/Migrate.php | 48 ++
main/app/system/Bakery/Command/MigrateRefresh.php | 52 ++
main/app/system/Bakery/Command/MigrateReset.php | 49 ++
main/app/system/Bakery/Command/MigrateRollback.php | 51 ++
main/app/system/Bakery/Command/Setup.php | 223 ++++++++
main/app/system/Bakery/Command/Test.php | 56 ++
main/app/system/Bakery/DatabaseTest.php | 52 ++
main/app/system/Bakery/Migration.php | 64 +++
main/app/system/Bakery/Migrator.php | 584 +++++++++++++++++++++
.../Database/Migrations/v410/MigrationTable.php | 59 +++
main/app/system/Database/Model/Migrations.php | 55 ++
main/app/system/Facade.php | 247 +++++++++
main/app/system/ServicesProvider.php | 104 ++++
main/app/system/SlimAppEvent.php | 29 +
main/app/system/Sprinkle/Sprinkle.php | 56 ++
main/app/system/Sprinkle/SprinkleManager.php | 236 +++++++++
main/app/system/UserFrosting.php | 187 +++++++
23 files changed, 2913 insertions(+)
create mode 100755 main/app/system/Bakery/Bakery.php
create mode 100755 main/app/system/Bakery/BaseCommand.php
create mode 100755 main/app/system/Bakery/Command/Bake.php
create mode 100755 main/app/system/Bakery/Command/BuildAssets.php
create mode 100755 main/app/system/Bakery/Command/ClearCache.php
create mode 100755 main/app/system/Bakery/Command/Debug.php
create mode 100755 main/app/system/Bakery/Command/Migrate.php
create mode 100755 main/app/system/Bakery/Command/MigrateRefresh.php
create mode 100755 main/app/system/Bakery/Command/MigrateReset.php
create mode 100755 main/app/system/Bakery/Command/MigrateRollback.php
create mode 100755 main/app/system/Bakery/Command/Setup.php
create mode 100755 main/app/system/Bakery/Command/Test.php
create mode 100755 main/app/system/Bakery/DatabaseTest.php
create mode 100755 main/app/system/Bakery/Migration.php
create mode 100755 main/app/system/Bakery/Migrator.php
create mode 100755 main/app/system/Database/Migrations/v410/MigrationTable.php
create mode 100755 main/app/system/Database/Model/Migrations.php
create mode 100755 main/app/system/Facade.php
create mode 100755 main/app/system/ServicesProvider.php
create mode 100755 main/app/system/SlimAppEvent.php
create mode 100755 main/app/system/Sprinkle/Sprinkle.php
create mode 100755 main/app/system/Sprinkle/SprinkleManager.php
create mode 100755 main/app/system/UserFrosting.php
(limited to 'main/app/system')
diff --git a/main/app/system/Bakery/Bakery.php b/main/app/system/Bakery/Bakery.php
new file mode 100755
index 0000000..8be8480
--- /dev/null
+++ b/main/app/system/Bakery/Bakery.php
@@ -0,0 +1,166 @@
+setupBaseSprinkleList();
+ }
+
+ // Create Symfony Console App
+ $this->app = new Application("UserFrosting Bakery", \UserFrosting\VERSION);
+
+ // Setup the sprinkles
+ $uf = new UserFrosting();
+
+ // Set argument as false, we are using the CLI
+ $uf->setupSprinkles(false);
+
+ // Get the container
+ $this->ci = $uf->getContainer();
+
+ // Add each commands to the Console App
+ $this->loadCommands();
+ }
+
+ /**
+ * Run the Symfony Console App
+ */
+ public function run()
+ {
+ $this->app->run();
+ }
+
+ /**
+ * Return the list of available commands for a specific sprinkle
+ */
+ protected function loadCommands()
+ {
+ // Get base Bakery command
+ $commands = $this->getBakeryCommands();
+
+ // Get the sprinkles commands
+ $sprinkles = $this->ci->sprinkleManager->getSprinkleNames();
+ foreach ($sprinkles as $sprinkle) {
+ $commands = $commands->merge($this->getSprinkleCommands($sprinkle));
+ }
+
+ // Add commands to the App
+ $commands->each(function($command) {
+ $instance = new $command();
+ $instance->setContainer($this->ci);
+ $this->app->add($instance);
+ });
+ }
+
+ /**
+ * Return the list of available commands for a specific sprinkle
+ * Sprinkles commands should be located in `src/Bakery/`
+ */
+ protected function getSprinkleCommands($sprinkle)
+ {
+ // Find all the migration files
+ $path = $this->commandDirectoryPath($sprinkle);
+ $files = glob($path . "*.php");
+ $commands = collect($files);
+
+ // Transform the path into a class names
+ $commands->transform(function ($file) use ($sprinkle, $path) {
+ $className = basename($file, '.php');
+ $sprinkleName = Str::studly($sprinkle);
+ $className = "\\UserFrosting\\Sprinkle\\".$sprinkleName."\\Bakery\\".$className;
+ return $className;
+ });
+
+ return $commands;
+ }
+
+ /**
+ * Return the list of available commands in system/Bakery/Command/
+ */
+ protected function getBakeryCommands()
+ {
+ // Find all the migration files
+ $files = glob(\UserFrosting\APP_DIR . "/system/Bakery/Command/" . "*.php");
+ $commands = collect($files);
+
+ // Transform the path into a class names
+ $commands->transform(function ($file) {
+ $className = basename($file, '.php');
+ $className = "\\UserFrosting\\System\\Bakery\\Command\\".$className;
+ return $className;
+ });
+
+ return $commands;
+ }
+
+ /**
+ * Returns the path of the Migration directory.
+ *
+ * @access protected
+ * @param mixed $sprinkleName
+ * @return void
+ */
+ protected function commandDirectoryPath($sprinkleName)
+ {
+ return \UserFrosting\SPRINKLES_DIR .
+ \UserFrosting\DS .
+ $sprinkleName .
+ \UserFrosting\DS .
+ \UserFrosting\SRC_DIR_NAME .
+ "/Bakery/";
+ }
+
+ /**
+ * Write the base Sprinkles schema file if it doesn't exist.
+ *
+ * @access protected
+ * @return void
+ */
+ protected function setupBaseSprinkleList()
+ {
+ $model = \UserFrosting\APP_DIR . '/sprinkles.example.json';
+ $destination = \UserFrosting\SPRINKLES_SCHEMA_FILE;
+ $sprinklesModelFile = @file_get_contents($model);
+ if ($sprinklesModelFile === false) {
+ $this->io->error("File `$sprinklesModelFile` not found. Please create '$destination' manually and try again.");
+ exit(1);
+ }
+
+ file_put_contents($destination, $sprinklesModelFile);
+
+ return $sprinklesModelFile;
+ }
+}
diff --git a/main/app/system/Bakery/BaseCommand.php b/main/app/system/Bakery/BaseCommand.php
new file mode 100755
index 0000000..1a59141
--- /dev/null
+++ b/main/app/system/Bakery/BaseCommand.php
@@ -0,0 +1,58 @@
+io = new SymfonyStyle($input, $output);
+ $this->projectRoot = \UserFrosting\ROOT_DIR;
+ }
+
+ /**
+ * Setup the global container object
+ */
+ public function setContainer(ContainerInterface $ci)
+ {
+ $this->ci = $ci;
+ }
+}
diff --git a/main/app/system/Bakery/Command/Bake.php b/main/app/system/Bakery/Command/Bake.php
new file mode 100755
index 0000000..5dc0e27
--- /dev/null
+++ b/main/app/system/Bakery/Command/Bake.php
@@ -0,0 +1,77 @@
+setName("bake")
+ ->setDescription("UserFrosting installation command")
+ ->setHelp("This command combine the debug, migrate and build-assets commands.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->writeln("{$this->title}");
+
+ $command = $this->getApplication()->find('setup');
+ $command->run($input, $output);
+
+ $command = $this->getApplication()->find('debug');
+ $command->run($input, $output);
+
+ $command = $this->getApplication()->find('migrate');
+ $command->run($input, $output);
+
+ $command = $this->getApplication()->find('create-admin');
+ $command->run($input, $output);
+
+ $command = $this->getApplication()->find('build-assets');
+ $command->run($input, $output);
+
+ $command = $this->getApplication()->find('clear-cache');
+ $command->run($input, $output);
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/BuildAssets.php b/main/app/system/Bakery/Command/BuildAssets.php
new file mode 100755
index 0000000..055fa43
--- /dev/null
+++ b/main/app/system/Bakery/Command/BuildAssets.php
@@ -0,0 +1,180 @@
+setName("build-assets")
+ ->setDescription("Build the assets using node and npm")
+ ->setHelp("The build directory contains the scripts and configuration files required to download Javascript, CSS, and other assets used by UserFrosting. This command will install Gulp, Bower, and several other required npm packages locally. With npm set up with all of its required packages, it can be use it to automatically download and install the assets in the correct directories. For more info, see https://learn.userfrosting.com/basics/installation")
+ ->addOption("compile", "c", InputOption::VALUE_NONE, "Compile the assets and asset bundles for production environment")
+ ->addOption("force", "f", InputOption::VALUE_NONE, "Force assets compilation by deleting cached data and installed assets before proceeding");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ // Display header,
+ $this->io->title("UserFrosting's Assets Builder");
+
+ // Set $path
+ $this->buildPath = $this->projectRoot . \UserFrosting\DS . \UserFrosting\BUILD_DIR_NAME;
+
+ // Delete cached data is requested
+ if ($input->getOption('force')) {
+ $this->clean();
+ }
+
+ // Perform tasks
+ $this->npmInstall();
+ $this->assetsInstall();
+
+ // Compile if requested
+ if ($input->getOption('compile') || $this->isProduction()) {
+ $this->buildAssets();
+ }
+
+ // Test the result
+ $this->checkAssets();
+
+ // If all went well and there's no fatal errors, we are successful
+ $this->io->success("Assets install looks successful");
+ }
+
+ /**
+ * Install npm package
+ *
+ * @access protected
+ * @return void
+ */
+ protected function npmInstall()
+ {
+ $this->io->section("Installing npm dependencies");
+ $this->io->writeln("> npm install");
+
+ // Temporarily change the working directory so we can install npm dependencies
+ $wd = getcwd();
+ chdir($this->buildPath);
+ passthru("npm install");
+ chdir($wd);
+ }
+
+ /**
+ * Perform UF Assets installation
+ *
+ * @access protected
+ * @return void
+ */
+ protected function assetsInstall()
+ {
+ $this->io->section("Installing assets bundles");
+ $this->io->writeln("> npm run uf-assets-install");
+ passthru("npm run uf-assets-install --prefix " . $this->buildPath);
+ }
+
+ /**
+ * Build the production bundle.
+ *
+ * @access protected
+ * @return void
+ */
+ protected function buildAssets()
+ {
+ $this->io->section("Building assets for production");
+
+ $this->io->writeln("> npm run uf-bundle-build");
+ passthru("npm run uf-bundle-build --prefix " . $this->buildPath);
+
+ $this->io->writeln("> npm run uf-bundle");
+ passthru("npm run uf-bundle --prefix " . $this->buildPath);
+
+ $this->io->writeln("> npm run uf-bundle-clean");
+ passthru("npm run uf-bundle-clean --prefix " . $this->buildPath);
+ }
+
+ /**
+ * Check that the assets where installed in the core sprinkles
+ *
+ * @access protected
+ * @return void
+ */
+ protected function checkAssets()
+ {
+ $this->io->section("Testing assets installation");
+
+ // Get path and vendor files
+ $vendorPath = \UserFrosting\SPRINKLES_DIR . "/core/assets/vendor/*";
+ $coreVendorFiles = glob($vendorPath);
+
+ if (!$coreVendorFiles){
+ $this->io->error("Assets installation seems to have failed. Directory `$vendorPath` is empty, but it shouldn't be. Check the above log for any errors.");
+ exit(1);
+ }
+
+ // Check that `bundle.result.json` is present in production mode
+ $config = $this->ci->config;
+ $resultFile = \UserFrosting\ROOT_DIR . \UserFrosting\DS . \UserFrosting\BUILD_DIR_NAME . \UserFrosting\DS . $config['assets.compiled.schema'];
+ if ($this->isProduction() && !file_exists($resultFile)) {
+ $this->io->error("Assets building seems to have failed. File `$resultFile` not found. This file is required for production envrionement. Check the above log for any errors.");
+ exit(1);
+ }
+ }
+
+ /**
+ * Run the `uf-clean` command to delete installed assets, delete compiled
+ * bundle config file and delete compiled assets
+ *
+ * @access protected
+ * @return void
+ */
+ protected function clean()
+ {
+ $this->io->section("Cleaning cached data");
+ $this->io->writeln("> npm run uf-clean");
+ passthru("npm run uf-clean --prefix " . $this->buildPath);
+ }
+
+ /**
+ * Return if the app is in production mode
+ *
+ * @access protected
+ * @return bool
+ */
+ protected function isProduction()
+ {
+ // N.B.: Need to touch the config service first to load dotenv values
+ $config = $this->ci->config;
+ $mode = getenv("UF_MODE") ?: '';
+
+ return ($mode == "production");
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/ClearCache.php b/main/app/system/Bakery/Command/ClearCache.php
new file mode 100755
index 0000000..d38f382
--- /dev/null
+++ b/main/app/system/Bakery/Command/ClearCache.php
@@ -0,0 +1,95 @@
+setName("clear-cache")
+ ->setDescription("Clears the application cache. Includes cache service, Twig and Router cached data");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("Clearing cache");
+
+ // Clear normal cache
+ $this->io->writeln(" > Clearing Illuminate cache instance", OutputInterface::VERBOSITY_VERBOSE);
+ $this->clearIlluminateCache();
+
+ // Clear Twig cache
+ $this->io->writeln(" > Clearing Twig cached data", OutputInterface::VERBOSITY_VERBOSE);
+ if (!$this->clearTwigCache()) {
+ $this->io->error("Failed to clear Twig cached data. Make sure you have write access to the `app/cache/twig` directory.");
+ exit(1);
+ }
+
+ // Clear router cache
+ $this->io->writeln(" > Clearing Router cache file", OutputInterface::VERBOSITY_VERBOSE);
+ if (!$this->clearRouterCache()) {
+ $file = $this->ci->config['settings.routerCacheFile'];
+ $this->io->error("Failed to delete Router cache file. Make sure you have write access to the `$file` file.");
+ exit(1);
+ }
+
+ $this->io->success("Cache cleared !");
+ }
+
+ /**
+ * Flush the cached data from the cache service
+ *
+ * @access protected
+ * @return void
+ */
+ protected function clearIlluminateCache()
+ {
+ $this->ci->cache->flush();
+ }
+
+ /**
+ * Clear the Twig cache using the Twig CacheHelper class
+ *
+ * @access protected
+ * @return bool true/false if operation is successfull
+ */
+ protected function clearTwigCache()
+ {
+ $cacheHelper = new CacheHelper($this->ci);
+ return $cacheHelper->clearCache();
+ }
+
+ /**
+ * Clear the Router cache data file
+ *
+ * @access protected
+ * @return bool true/false if operation is successfull
+ */
+ protected function clearRouterCache()
+ {
+ return $this->ci->router->clearCache();
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/Debug.php b/main/app/system/Bakery/Command/Debug.php
new file mode 100755
index 0000000..4e8a3e4
--- /dev/null
+++ b/main/app/system/Bakery/Command/Debug.php
@@ -0,0 +1,185 @@
+setName("debug")
+ ->setDescription("Test the UserFrosting installation and setup the database")
+ ->setHelp("This command is used to check if the various dependencies of UserFrosting are met and display useful debugging information. \nIf any error occurs, check out the online documentation for more info about that error. \nThis command also provide the necessary tools to setup the database credentials");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ // Display header,
+ $this->io->title("UserFrosting");
+ $this->io->writeln("UserFrosing version : " . \UserFrosting\VERSION);
+ $this->io->writeln("OS Name : " . php_uname('s'));
+ $this->io->writeln("Project Root : {$this->projectRoot}");
+
+ // Need to touch the config service first to load dotenv values
+ $config = $this->ci->config;
+ $this->io->writeln("Environment mode : " . getenv("UF_MODE"));
+
+ // Perform tasks
+ $this->checkPhpVersion();
+ $this->checkNodeVersion();
+ $this->checkNpmVersion();
+ $this->listSprinkles();
+ $this->showConfig();
+ $this->checkDatabase();
+
+ // If all went well and there's no fatal errors, we are ready to bake
+ $this->io->success("Ready to bake !");
+ }
+
+ /**
+ * Check the minimum version of php.
+ * This is done by composer itself, but we do it again for good mesure
+ *
+ * @access public
+ * @return void
+ */
+ protected function checkPhpVersion()
+ {
+ $this->io->writeln("PHP Version : " . phpversion());
+ if (version_compare(phpversion(), \UserFrosting\PHP_MIN_VERSION, '<')) {
+ $this->io->error("UserFrosting requires php version ".\UserFrosting\PHP_MIN_VERSION." or above. You'll need to update you PHP version before you can continue.");
+ exit(1);
+ }
+ }
+
+ /**
+ * Check the minimum version requirement of Node installed
+ *
+ * @access public
+ * @return void
+ */
+ protected function checkNodeVersion()
+ {
+ $npmVersion = trim(exec('node -v'));
+ $this->io->writeln("Node Version : $npmVersion");
+
+ if (version_compare($npmVersion, 'v4', '<')) {
+ $this->io->error("UserFrosting requires Node version 4.x or above. Check the documentation for more details.");
+ exit(1);
+ }
+ }
+
+ /**
+ * Check the minimum version requirement for Npm
+ *
+ * @access public
+ * @return void
+ */
+ protected function checkNpmVersion()
+ {
+ $npmVersion = trim(exec('npm -v'));
+ $this->io->writeln("NPM Version : $npmVersion");
+
+ if (version_compare($npmVersion, '3', '<')) {
+ $this->io->error("UserFrosting requires npm version 3.x or above. Check the documentation for more details.");
+ exit(1);
+ }
+ }
+
+ /**
+ * List all sprinkles defined in the Sprinkles schema file,
+ * making sure this file exist at the same time
+ *
+ * @access protected
+ * @return void
+ */
+ protected function listSprinkles()
+ {
+ // Check for Sprinkles schema file
+ $path = \UserFrosting\SPRINKLES_SCHEMA_FILE;
+ $sprinklesFile = @file_get_contents($path);
+ if ($sprinklesFile === false) {
+ $this->io->error("The file `$path` not found.");
+ }
+
+ // List installed sprinkles
+ $sprinkles = json_decode($sprinklesFile)->base;
+ $this->io->section("Loaded sprinkles");
+ $this->io->listing($sprinkles);
+
+ // Throw fatal error if the `core` sprinkle is missing
+ if (!in_array("core", $sprinkles)) {
+ $this->io->error("The `core` sprinkle is missing from the 'sprinkles.json' file.");
+ exit(1);
+ }
+ }
+
+ /**
+ * Check the database connexion and setup the `.env` file if we can't
+ * connect and there's no one found.
+ *
+ * @access protected
+ * @return void
+ */
+ protected function checkDatabase()
+ {
+ $this->io->section("Testing database connection...");
+
+ try {
+ $this->testDB();
+ $this->io->writeln("Database connection successful");
+ return;
+ } catch (\Exception $e) {
+ $error = $e->getMessage();
+ $this->io->error($error);
+ exit(1);
+ }
+ }
+
+ /**
+ * Display database config as for debug purposes
+ *
+ * @access protected
+ * @return void
+ */
+ protected function showConfig()
+ {
+ // Get config
+ $config = $this->ci->config;
+
+ // Display database info
+ $this->io->section("Database config");
+ $this->io->writeln([
+ "DRIVER : " . $config['db.default.driver'],
+ "HOST : " . $config['db.default.host'],
+ "PORT : " . $config['db.default.port'],
+ "DATABASE : " . $config['db.default.database'],
+ "USERNAME : " . $config['db.default.username'],
+ "PASSWORD : " . ($config['db.default.password'] ? "*********" : "")
+ ]);
+ }
+}
diff --git a/main/app/system/Bakery/Command/Migrate.php b/main/app/system/Bakery/Command/Migrate.php
new file mode 100755
index 0000000..c0a5b4f
--- /dev/null
+++ b/main/app/system/Bakery/Command/Migrate.php
@@ -0,0 +1,48 @@
+setName("migrate")
+ ->setDescription("Perform database migration")
+ ->setHelp("This command runs all the pending database migrations.")
+ ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("UserFrosting's Migrator");
+
+ $pretend = $input->getOption('pretend');
+
+ $migrator = new Migrator($this->io, $this->ci);
+ $migrator->runUp($pretend);
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/MigrateRefresh.php b/main/app/system/Bakery/Command/MigrateRefresh.php
new file mode 100755
index 0000000..3d18c41
--- /dev/null
+++ b/main/app/system/Bakery/Command/MigrateRefresh.php
@@ -0,0 +1,52 @@
+setName("migrate:refresh")
+ ->setDescription("Rollback the last migration operation and run it up again")
+ ->addOption('steps', 's', InputOption::VALUE_REQUIRED, 'Number of steps to rollback', 1)
+ ->addOption('sprinkle', null, InputOption::VALUE_REQUIRED, 'The sprinkle to rollback', "")
+ ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("Migration refresh");
+
+ $step = $input->getOption('steps');
+ $sprinkle = $input->getOption('sprinkle');
+ $pretend = $input->getOption('pretend');
+
+ $migrator = new Migrator($this->io, $this->ci);
+ $migrator->runDown($step, $sprinkle, $pretend);
+ $migrator->runUp($pretend);
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/MigrateReset.php b/main/app/system/Bakery/Command/MigrateReset.php
new file mode 100755
index 0000000..9e38cbb
--- /dev/null
+++ b/main/app/system/Bakery/Command/MigrateReset.php
@@ -0,0 +1,49 @@
+setName("migrate:reset")
+ ->setDescription("Reset the whole database to an empty state")
+ ->addOption('sprinkle', null, InputOption::VALUE_REQUIRED, 'The sprinkle to rollback', "")
+ ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("Migration reset");
+
+ $sprinkle = $input->getOption('sprinkle');
+ $pretend = $input->getOption('pretend');
+
+ $migrator = new Migrator($this->io, $this->ci);
+ $migrator->runDown(-1, $sprinkle, $pretend);
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/MigrateRollback.php b/main/app/system/Bakery/Command/MigrateRollback.php
new file mode 100755
index 0000000..916f5ee
--- /dev/null
+++ b/main/app/system/Bakery/Command/MigrateRollback.php
@@ -0,0 +1,51 @@
+setName("migrate:rollback")
+ ->setDescription("Rollback last database migration")
+ ->addOption('steps', 's', InputOption::VALUE_REQUIRED, 'Number of steps to rollback', 1)
+ ->addOption('sprinkle', null, InputOption::VALUE_REQUIRED, 'The sprinkle to rollback', "")
+ ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("Migration rollback");
+
+ $step = $input->getOption('steps');
+ $sprinkle = $input->getOption('sprinkle');
+ $pretend = $input->getOption('pretend');
+
+ $migrator = new Migrator($this->io, $this->ci);
+ $migrator->runDown($step, $sprinkle, $pretend);
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Command/Setup.php b/main/app/system/Bakery/Command/Setup.php
new file mode 100755
index 0000000..b489ce2
--- /dev/null
+++ b/main/app/system/Bakery/Command/Setup.php
@@ -0,0 +1,223 @@
+setName("setup")
+ ->setDescription("UserFrosting configuration wizard")
+ ->setHelp("Helper command to setup the database and email configuration. This can also be done manually by editing the app/.env file or using global server environment variables.")
+ ->addOption("force", "f", InputOption::VALUE_NONE, "If `.env` file exist, force setup to run again");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ // Get config
+ $config = $this->ci->config;
+
+ // Get options
+ $force = $input->getOption('force');
+
+ // Display header,
+ $this->io->title("UserFrosting's Setup Wizard");
+
+ // Check if the .env file exist.
+ if (!$force && file_exists($this->envPath)) {
+ $this->io->note("File `{$this->envPath}` already exist. Use the `php bakery setup -f` command to force setup to run again.");
+ return;
+ }
+
+ // There might not be any `.env` file because there may be some custom config or global env values defined on the server.
+ // We'll check for that. If the configs are empty, we'll assume nothing is defined and go strait to setup.
+ if (!$force && $config["db.default.host"] != "" && $config["db.default.database"] != "" && $config["db.default.username"] != "") {
+ $this->io->note("File `{$this->envPath}` was not found, but some database configuration variables are present. Global system environment variables might be defined. If this is not right, use -f option to force setup to run.");
+ return;
+ }
+
+ //Goto setup
+ $this->setupEnv();
+ }
+
+ /**
+ * Setup the `.env` file.
+ *
+ * @access public
+ * @return void
+ */
+ public function setupEnv()
+ {
+ // Get config
+ $config = $this->ci->config;
+
+ // Get the db driver choices
+ $drivers = $this->databaseDrivers();
+
+
+ // Ask the questions
+ $this->io->section("Setting up database");
+ $this->io->note("Database credentials will be saved in `app/.env`");
+
+ $driver = $this->io->choice("Database type", $drivers->pluck('name')->toArray());
+ $driver = $drivers->where('name', $driver)->first();
+
+ $driverName = $driver['driver'];
+ $defaultDBName = $driver['defaultDBName'];
+
+ if ($driverName == 'sqlite') {
+ $name = $this->io->ask("Database name", $defaultDBName);
+
+ $dbParams = [
+ 'driver' => $driverName,
+ 'database' => $name
+ ];
+ } else {
+ $defaultPort = $driver['defaultPort'];
+
+ $host = $this->io->ask("Hostname", "localhost");
+ $port = $this->io->ask("Port", $defaultPort);
+ $name = $this->io->ask("Database name", $defaultDBName);
+ $user = $this->io->ask("Username", "userfrosting");
+ $password = $this->io->askHidden("Password", function ($password) {
+ // Use custom validator to accept empty password
+ return $password;
+ });
+
+ $dbParams = [
+ 'driver' => $driverName,
+ 'host' => $host,
+ 'port' => $port,
+ 'database' => $name,
+ 'username' => $user,
+ 'password' => $password,
+ 'charset' => $config['db.default.charset']
+ ];
+ }
+
+ // Setup a new db connection
+ $capsule = new Capsule;
+ $capsule->addConnection($dbParams);
+
+ // Test the db connexion.
+ try {
+ $conn = $capsule->getConnection();
+ $conn->getPdo();
+ $this->io->success("Database connection successful");
+ $success = true;
+ } catch (\PDOException $e) {
+ $message = "Could not connect to the database '{$dbParams['username']}@{$dbParams['host']}/{$dbParams['database']}':".PHP_EOL;
+ $message .= "Exception: " . $e->getMessage() . PHP_EOL.PHP_EOL;
+ $message .= "Please check your database configuration and/or google the exception shown above and run the command again.";
+ $this->io->error($message);
+ exit(1);
+ }
+
+ // Ask for the smtp values now
+ $this->io->section("Enter your SMTP credentials");
+ $this->io->write("This is used to send emails from the system. Edit `app/.env` if you have problems with this later.");
+ $smtpHost = $this->io->ask("SMTP Host", "host.example.com");
+ $smtpUser = $this->io->ask("SMTP User", "relay@example.com");
+ $smtpPassword = $this->io->askHidden("SMTP Password", function ($password) {
+ // Use custom validator to accept empty password
+ return $password;
+ });
+
+ if ($driverName == 'sqlite') {
+ $fileContent = [
+ "UF_MODE=\"\"\n",
+ "DB_DRIVER=\"{$dbParams['driver']}\"\n",
+ "DB_NAME=\"{$dbParams['database']}\"\n",
+ "SMTP_HOST=\"$smtpHost\"\n",
+ "SMTP_USER=\"$smtpUser\"\n",
+ "SMTP_PASSWORD=\"$smtpPassword\"\n"
+ ];
+ } else {
+ $fileContent = [
+ "UF_MODE=\"\"\n",
+ "DB_DRIVER=\"{$dbParams['driver']}\"\n",
+ "DB_HOST=\"{$dbParams['host']}\"\n",
+ "DB_PORT=\"{$dbParams['port']}\"\n",
+ "DB_NAME=\"{$dbParams['database']}\"\n",
+ "DB_USER=\"{$dbParams['username']}\"\n",
+ "DB_PASSWORD=\"{$dbParams['password']}\"\n",
+ "SMTP_HOST=\"$smtpHost\"\n",
+ "SMTP_USER=\"$smtpUser\"\n",
+ "SMTP_PASSWORD=\"$smtpPassword\"\n"
+ ];
+ }
+
+ // Let's save this config
+ file_put_contents($this->envPath, $fileContent);
+
+ // At this point, `$this->uf` is still using the old configs.
+ // We need to refresh the `db.default` config values
+ $newConfig = array_merge($config['db.default'], $dbParams);
+ $this->ci->config->set("db.default", $newConfig);
+ }
+
+ /**
+ * Return the database choices for the env setup.
+ *
+ * @access protected
+ * @return void
+ */
+ protected function databaseDrivers()
+ {
+ return collect([
+ [
+ "driver" => "mysql",
+ "name" => "MySQL / MariaDB",
+ "defaultDBName" => "userfrosting",
+ "defaultPort" => 3306
+ ],
+ [
+ "driver" => "pgsql",
+ "name" => "ProgreSQL",
+ "defaultDBName" => "userfrosting",
+ "defaultPort" => 5432
+ ],
+ [
+ "driver" => "sqlsrv",
+ "name" => "SQL Server",
+ "defaultDBName" => "userfrosting",
+ "defaultPort" => 1433
+ ],
+ [
+ "driver" => "sqlite",
+ "name" => "SQLite",
+ "defaultDBName" => \UserFrosting\DB_DIR . \UserFrosting\DS . 'userfrosting.db',
+ "defaultPort" => null
+ ]
+ ]);
+ }
+}
diff --git a/main/app/system/Bakery/Command/Test.php b/main/app/system/Bakery/Command/Test.php
new file mode 100755
index 0000000..553fddd
--- /dev/null
+++ b/main/app/system/Bakery/Command/Test.php
@@ -0,0 +1,56 @@
+setName("test")
+ ->setDescription("Run tests")
+ ->setHelp("Run php unit tests");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("UserFrosting's Tester");
+
+ // Get command
+ $command = \UserFrosting\VENDOR_DIR . "/bin/phpunit --colors=always";
+ if ($output->isVerbose() || $output->isVeryVerbose()) {
+ $command .= " -v";
+ }
+
+ // Execute
+ $this->io->writeln("> $command");
+ passthru($command);
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/DatabaseTest.php b/main/app/system/Bakery/DatabaseTest.php
new file mode 100755
index 0000000..0e4f3bf
--- /dev/null
+++ b/main/app/system/Bakery/DatabaseTest.php
@@ -0,0 +1,52 @@
+ci->db;
+
+ // Get config
+ $config = $this->ci->config;
+
+ // Check params are valids
+ $dbParams = $config['db.default'];
+ if (!$dbParams) {
+ throw new \Exception("'default' database connection not found. Please double-check your configuration.");
+ }
+
+ // Test database connection directly using PDO
+ try {
+ Capsule::connection()->getPdo();
+ } catch (\PDOException $e) {
+ $message = "Could not connect to the database '{$dbParams['username']}@{$dbParams['host']}/{$dbParams['database']}':".PHP_EOL;
+ $message .= "Exception: " . $e->getMessage() . PHP_EOL.PHP_EOL;
+ $message .= "Please check your database configuration and/or google the exception shown above and run command again.";
+ throw new \Exception($message);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/main/app/system/Bakery/Migration.php b/main/app/system/Bakery/Migration.php
new file mode 100755
index 0000000..e6c6ae0
--- /dev/null
+++ b/main/app/system/Bakery/Migration.php
@@ -0,0 +1,64 @@
+schema = $schema;
+ $this->io = $io;
+ }
+
+ /**
+ * Method to apply changes to the database
+ */
+ public function up() {}
+
+ /**
+ * Method to revert changes applied by the `up` method
+ */
+ public function down() {}
+
+ /**
+ * Method to seed new information to the database
+ */
+ public function seed() {}
+}
diff --git a/main/app/system/Bakery/Migrator.php b/main/app/system/Bakery/Migrator.php
new file mode 100755
index 0000000..611f73f
--- /dev/null
+++ b/main/app/system/Bakery/Migrator.php
@@ -0,0 +1,584 @@
+io = $io;
+ $this->ci = $ci;
+
+ // Start by testing the DB connexion, just in case
+ try {
+ $this->io->writeln("Testing database connection", OutputInterface::VERBOSITY_VERBOSE);
+ $this->testDB();
+ $this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE);
+ } catch (\Exception $e) {
+ $this->io->error($e->getMessage());
+ exit(1);
+ }
+
+ // Get schema required to run the table blueprints
+ $this->schema = Capsule::schema();
+
+ // Make sure the setup table exist
+ $this->setupVersionTable();
+ }
+
+ /**
+ * Run all the migrations available
+ *
+ * @access public
+ * @param bool $pretend (default: false)
+ * @return void
+ */
+ public function runUp($pretend = false)
+ {
+ // Get installed migrations and pluck by class name. We only need this for now
+ $migrations = Migrations::get();
+ $this->installed = $migrations->pluck('migration');
+
+ $this->io->writeln("\nInstalled migrations:", OutputInterface::VERBOSITY_VERBOSE);
+ $this->io->writeln($this->installed->toArray(), OutputInterface::VERBOSITY_VERBOSE);
+
+ // Get pending migrations
+ $this->io->section("Fetching available migrations");
+ $this->pending = $this->getPendingMigrations();
+
+ // If there's no pending migration, don't need to go further
+ if ($this->pending->isEmpty()) {
+ $this->io->success("Nothing to migrate !");
+ return;
+ }
+
+ // Resolve the dependencies
+ $this->resolveDependencies();
+
+ // If there are any unfulfillable migration, we can't continue
+ if (!$this->unfulfillable->isEmpty()) {
+
+ $msg = "\nSome migrations dependencies can't be met. Check those migrations for unmet dependencies and try again:";
+
+ foreach ($this->unfulfillable as $migration) {
+ $msg .= "\n{$migration->className} depends on \n - ";
+ $msg .= implode("\n - ", $migration->dependencies);
+ $msg .= "\n";
+ }
+
+ $this->io->error($msg);
+ exit(1);
+ }
+
+ // Ready to run !
+ $this->io->section("Running migrations");
+
+ if ($pretend) {
+ $this->io->note("Running migration in pretend mode");
+ }
+
+ // We have a list of fulfillable migration, we run them up!
+ foreach ($this->fulfillable as $migration) {
+ $this->io->write("\n> Migrating {$migration->className}...");
+
+ if ($pretend) {
+ $this->io->newLine();
+ $this->pretendToRun($migration, 'up');
+ } else {
+ $migration->up();
+ $migration->seed();
+ $this->log($migration);
+ $this->io->writeln(" Done!");
+ }
+ }
+
+ // If all went well and there's no fatal errors, we are ready to bake
+ $this->io->success("Migration successful !");
+ }
+
+ /**
+ * Rollback the last btach of migrations.
+ *
+ * @access public
+ * @param int $step (default: 1). Number of batch we will be going back. -1 revert all migrations
+ * @param string $sprinkle (default: "") Limit rollback to a specific sprinkle
+ * @param bool $pretend (default: false)
+ * @return void
+ */
+ public function runDown($step = 1, $sprinkle = "", $pretend = false)
+ {
+ // Can't go furhter down than 1 step
+ if ($step <= 0 && $step != -1) {
+ throw new \InvalidArgumentException("Step can't be 0 or less");
+ }
+
+ // Get last batch number
+ $batch = $this->getNextBatchNumber();
+
+ // Calculate the number of steps back we need to take
+ if ($step == -1) {
+ $stepsBack = 1;
+ $this->io->warning("Rolling back all migrations");
+ } else {
+ $stepsBack = max($batch - $step, 1);
+ $this->io->note("Rolling back $step steps to batch $stepsBack", OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ // Get installed migrations
+ $migrations = Migrations::orderBy("id", "desc")->where('batch', '>=', $stepsBack);
+
+ // Add the sprinkle requirement too
+ if ($sprinkle != "") {
+ $this->io->note("Rolling back sprinkle `$sprinkle`", OutputInterface::VERBOSITY_VERBOSE);
+ $migrations->where('sprinkle', $sprinkle);
+ }
+
+ // Run query
+ $migrations = $migrations->get();
+
+ // If there's nothing to rollback, stop here
+ if ($migrations->isEmpty()) {
+ $this->io->writeln("Nothing to rollback");
+ exit(1);
+ }
+
+ // Get pending migrations
+ $this->io->writeln("Migration to rollback:");
+ $this->io->listing($migrations->pluck('migration')->toArray());
+
+ // Ask confirmation to continue.
+ if (!$pretend && !$this->io->confirm("Continue?", false)) {
+ exit(1);
+ }
+
+ if ($pretend) {
+ $this->io->note("Rolling back in pretend mode");
+ }
+
+ // Loop again to run down each migration
+ foreach ($migrations as $migration) {
+
+ // Check if those migration class are available
+ if (!class_exists($migration->migration)) {
+ $this->io->warning("Migration class {$migration->migration} doesn't exist.");
+ continue;
+ }
+
+ $this->io->write("> Rolling back {$migration->migration}...");
+ $migrationClass = $migration->migration;
+ $instance = new $migrationClass($this->schema, $this->io);
+
+ if ($pretend) {
+ $this->io->newLine();
+ $this->pretendToRun($instance, 'down');
+ } else {
+ $instance->down();
+ $migration->delete();
+ $this->io->writeln(" Done!");
+ }
+
+ $this->io->newLine();
+ }
+
+ // If all went well and there's no fatal errors, we are ready to bake
+ $this->io->success("Rollback successful !");
+ }
+
+ /**
+ * Pretend to run migration class.
+ *
+ * @access protected
+ * @param mixed $migration
+ * @param string $method up/down
+ */
+ protected function pretendToRun($migration, $method)
+ {
+ foreach ($this->getQueries($migration, $method) as $query) {
+ $this->io->writeln($query['query'], OutputInterface::VERBOSITY_VERBOSE);
+ }
+ }
+
+ /**
+ * Return all of the queries that would be run for a migration.
+ *
+ * @access protected
+ * @param mixed $migration
+ * @param string $method up/down
+ * @return void
+ */
+ protected function getQueries($migration, $method)
+ {
+ $db = $this->schema->getConnection();
+
+ return $db->pretend(function () use ($migration, $method) {
+ if (method_exists($migration, $method)) {
+ $migration->{$method}();
+ }
+ });
+ }
+
+ /**
+ * Get pending migrations by looking at all the migration files
+ * and finding the one not yet runed by compairing with the ran migrations
+ *
+ * @access protected
+ * @return void
+ */
+ protected function getPendingMigrations()
+ {
+ $pending = collect([]);
+
+ // Get the sprinkle list
+ $sprinkles = $this->ci->sprinkleManager->getSprinkleNames();
+
+ // Loop all the sprinkles to find their pending migrations
+ foreach ($sprinkles as $sprinkle) {
+
+ $this->io->writeln("> Fetching from `$sprinkle`");
+
+ // We get all the migrations. This will return them as a colleciton of class names
+ $migrations = $this->getMigrations($sprinkle);
+
+ // We filter the available migration by removing the one that have already been run
+ // This reject the class name found in the installed collection
+ $migrations = $migrations->reject(function ($value, $key) {
+ return $this->installed->contains($value);
+ });
+
+ // Load each class
+ foreach ($migrations as $migrationClass) {
+
+ // Make sure the class exist
+ if (!class_exists($migrationClass)) {
+ throw new BadClassNameException("Unable to find the migration class '$migrationClass'." );
+ }
+
+ // Load the migration class
+ $migration = new $migrationClass($this->schema, $this->io);
+
+ //Set the sprinkle
+ $migration->sprinkle = $sprinkle;
+
+ // Also set the class name. We could find it using ::class, but this
+ // will make it easier to manipulate the collection
+ $migration->className = $migrationClass;
+
+ // Add it to the pending list
+ $pending->push($migration);
+ }
+ }
+
+ // Get pending migrations
+ $pendingArray = ($pending->pluck('className')->toArray()) ?: "";
+ $this->io->writeln("\nPending migrations:", OutputInterface::VERBOSITY_VERBOSE);
+ $this->io->writeln($pendingArray, OutputInterface::VERBOSITY_VERBOSE);
+
+ return $pending;
+ }
+
+ /**
+ * Get the list of migrations avaiables in the filesystem.
+ * Return a list of resolved className
+ *
+ * @access public
+ * @param string $sprinkleName
+ * @return void
+ */
+ public function getMigrations($sprinkle)
+ {
+ // Find all the migration files
+ $path = $this->migrationDirectoryPath($sprinkle);
+ $files = glob($path . "*/*.php");
+
+ // Transform the array in a collection
+ $migrations = collect($files);
+
+ // We transform the path into a migration object
+ $migrations->transform(function ($file) use ($sprinkle, $path) {
+ // Deconstruct the path
+ $migration = str_replace($path, "", $file);
+ $className = basename($file, '.php');
+ $sprinkleName = Str::studly($sprinkle);
+ $version = str_replace("/$className.php", "", $migration);
+
+ // Reconstruct the classname
+ $className = "\\UserFrosting\\Sprinkle\\".$sprinkleName."\\Database\\Migrations\\".$version."\\".$className;
+
+ return $className;
+ });
+
+ return $migrations;
+ }
+
+ /**
+ * Resolve all the dependencies for all the pending migrations
+ * This function fills in the `fullfillable` and `unfulfillable` list
+ *
+ * @access protected
+ * @return void
+ */
+ protected function resolveDependencies()
+ {
+ $this->io->writeln("\nResolving migrations dependencies...", OutputInterface::VERBOSITY_VERBOSE);
+
+ // Reset fulfillable/unfulfillable lists
+ $this->fulfillable = collect([]);
+ $this->unfulfillable = collect([]);
+
+ // Loop pending and check for dependencies
+ foreach ($this->pending as $migration) {
+ $this->validateClassDependencies($migration);
+ }
+
+ $fulfillable = ($this->fulfillable->pluck('className')->toArray()) ?: "";
+ $this->io->writeln("\nFulfillable migrations:", OutputInterface::VERBOSITY_VERBOSE);
+ $this->io->writeln($fulfillable, OutputInterface::VERBOSITY_VERBOSE);
+
+ $unfulfillable = ($this->unfulfillable->pluck('className')->toArray()) ?: "";
+ $this->io->writeln("\nUnfulfillable migrations:", OutputInterface::VERBOSITY_VERBOSE);
+ $this->io->writeln($unfulfillable, OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ /**
+ * Check if a migration dependencies are met.
+ * To test if a migration is fulfillable, the class must :
+ * Already been installed OR exist and have all it's dependencies met
+ *
+ * @access protected
+ * @param $migration
+ * @return bool true/false if all conditions are met
+ */
+ protected function validateClassDependencies($migration)
+ {
+ $this->io->writeln("> Checking dependencies for {$migration->className}", OutputInterface::VERBOSITY_VERBOSE);
+
+ // If it's already marked as fulfillable, it's fulfillable
+ // Return true directly (it's already marked)
+ if ($this->fulfillable->contains($migration)) {
+ return true;
+ }
+
+ // If it's already marked as unfulfillable, it's unfulfillable
+ // Return false directly (it's already marked)
+ if ($this->unfulfillable->contains($migration)) {
+ return false;
+ }
+
+ // If it's already run, it's fulfillable
+ // Mark it as such for next time it comes up in this loop
+ if ($this->installed->contains($migration->className)) {
+ return $this->markAsFulfillable($migration);
+ }
+
+ // Loop dependencies. If one is not fulfillable, then this one is not either
+ foreach ($migration->dependencies as $dependencyClass) {
+
+ // The dependency might already be installed. Check that first
+ if ($this->installed->contains($dependencyClass)) {
+ continue;
+ }
+
+ // Try to find it in the `pending` list. Cant' find it? Then it's not fulfillable
+ $dependency = $this->pending->where('className', $dependencyClass)->first();
+
+ // Check migration dependencies of this one right now
+ // If ti's not fullfillable, then this one isn't either
+ if (!$dependency || !$this->validateClassDependencies($dependency)) {
+ return $this->markAsUnfulfillable($migration);
+ }
+ }
+
+ // If no dependencies returned false, it's fulfillable
+ return $this->markAsFulfillable($migration);
+ }
+
+ /**
+ * Mark a dependency as fulfillable.
+ * Removes it from the pending list and add it to the fulfillable list
+ *
+ * @access protected
+ * @param $migration
+ * @return true
+ */
+ protected function markAsFulfillable($migration)
+ {
+ $this->fulfillable->push($migration);
+ return true;
+ }
+
+ /**
+ * Mark a dependency as unfulfillable.
+ * Removes it from the pending list and add it to the unfulfillable list
+ *
+ * @access protected
+ * @param $migration
+ * @return false
+ */
+ protected function markAsUnfulfillable($migration)
+ {
+ $this->unfulfillable->push($migration);
+ return false;
+ }
+
+ /**
+ * Log that a migration was run.
+ *
+ * @access public
+ * @param mixed $migration
+ * @return void
+ */
+ protected function log($migration)
+ {
+ // Get the next batch number if not defined
+ if (!$this->batch) {
+ $this->batch = $this->getNextBatchNumber();
+ }
+
+ $log = new Migrations([
+ 'sprinkle' => $migration->sprinkle,
+ 'migration' => $migration->className,
+ 'batch' => $this->batch
+ ]);
+ $log->save();
+ }
+
+ /**
+ * Return the next batch number from the db.
+ * Batch number is used to group together migration run in the same operation
+ *
+ * @access public
+ * @return int the next batch number
+ */
+ public function getNextBatchNumber()
+ {
+ $batch = Migrations::max('batch');
+ return ($batch) ? $batch + 1 : 1;
+ }
+
+ /**
+ * Create the migration history table if needed.
+ * Also check if the tables requires migrations
+ * We run the migration file manually for this one
+ *
+ * @access public
+ * @return void
+ */
+ protected function setupVersionTable()
+ {
+ // Check if the `migrations` table exist. Create it manually otherwise
+ if (!$this->schema->hasColumn($this->table, 'id')) {
+ $this->io->section("Creating the `{$this->table}` table");
+
+ $migration = new \UserFrosting\System\Database\Migrations\v410\MigrationTable($this->schema, $this->io);
+ $migration->up();
+
+ $this->io->success("Table `{$this->table}` created");
+ }
+ }
+
+ /**
+ * Returns the path of the Migration directory.
+ *
+ * @access protected
+ * @param mixed $sprinkleName
+ * @return void
+ */
+ protected function migrationDirectoryPath($sprinkleName)
+ {
+ $path = \UserFrosting\SPRINKLES_DIR .
+ \UserFrosting\DS .
+ $sprinkleName .
+ \UserFrosting\DS .
+ \UserFrosting\SRC_DIR_NAME .
+ "/Database/Migrations/";
+
+ return $path;
+ }
+}
diff --git a/main/app/system/Database/Migrations/v410/MigrationTable.php b/main/app/system/Database/Migrations/v410/MigrationTable.php
new file mode 100755
index 0000000..fb833df
--- /dev/null
+++ b/main/app/system/Database/Migrations/v410/MigrationTable.php
@@ -0,0 +1,59 @@
+schema->create('migrations', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('sprinkle');
+ $table->string('migration');
+ $table->integer('batch');
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ });
+
+ // Drop the old `version` table if found
+ if ($this->schema->hasTable('version')) {
+ $this->schema->drop('version');
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('migrations');
+ }
+}
diff --git a/main/app/system/Database/Model/Migrations.php b/main/app/system/Database/Model/Migrations.php
new file mode 100755
index 0000000..6a0942e
--- /dev/null
+++ b/main/app/system/Database/Model/Migrations.php
@@ -0,0 +1,55 @@
+where('sprinkle', $sprinkleName);
+ }
+}
diff --git a/main/app/system/Facade.php b/main/app/system/Facade.php
new file mode 100755
index 0000000..0d1ad82
--- /dev/null
+++ b/main/app/system/Facade.php
@@ -0,0 +1,247 @@
+$name);
+ static::$container->$name = $instance;
+ }
+
+ /**
+ * Initiate a mock expectation on the facade.
+ *
+ * @param mixed
+ * @return \Mockery\Expectation
+ */
+ public static function shouldReceive()
+ {
+ $name = static::getFacadeAccessor();
+
+ if (static::isMock()) {
+ $mock = static::$resolvedInstance[$name];
+ } else {
+ $mock = static::createFreshMockInstance($name);
+ }
+
+ return call_user_func_array([$mock, 'shouldReceive'], func_get_args());
+ }
+
+ /**
+ * Create a fresh mock instance for the given class.
+ *
+ * @param string $name
+ * @return \Mockery\Expectation
+ */
+ protected static function createFreshMockInstance($name)
+ {
+ static::$resolvedInstance[$name] = $mock = static::createMockByName($name);
+
+ $mock->shouldAllowMockingProtectedMethods();
+
+ if (isset(static::$container)) {
+ static::$container->$name = $mock;
+ }
+
+ return $mock;
+ }
+
+ /**
+ * Create a fresh mock instance for the given class.
+ *
+ * @param string $name
+ * @return \Mockery\Expectation
+ */
+ protected static function createMockByName($name)
+ {
+ $class = static::getMockableClass($name);
+
+ return $class ? Mockery::mock($class) : Mockery::mock();
+ }
+
+ /**
+ * Determines whether a mock is set as the instance of the facade.
+ *
+ * @return bool
+ */
+ protected static function isMock()
+ {
+ $name = static::getFacadeAccessor();
+
+ return isset(static::$resolvedInstance[$name]) && static::$resolvedInstance[$name] instanceof MockInterface;
+ }
+
+ /**
+ * Get the mockable class for the bound instance.
+ *
+ * @return string|null
+ */
+ protected static function getMockableClass()
+ {
+ if ($root = static::getFacadeRoot()) {
+ return get_class($root);
+ }
+ }
+
+ /**
+ * Get the root object behind the facade.
+ *
+ * @return mixed
+ */
+ public static function getFacadeRoot()
+ {
+ return static::resolveFacadeInstance(static::getFacadeAccessor());
+ }
+
+ /**
+ * Get the registered name of the component.
+ *
+ * @return string
+ *
+ * @throws \RuntimeException
+ */
+ protected static function getFacadeAccessor()
+ {
+ throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
+ }
+
+ /**
+ * Resolve the facade root instance from the container.
+ *
+ * @param string|object $name
+ * @return mixed
+ */
+ protected static function resolveFacadeInstance($name)
+ {
+ if (is_object($name)) {
+ return $name;
+ }
+
+ if (isset(static::$resolvedInstance[$name])) {
+ return static::$resolvedInstance[$name];
+ }
+
+ return static::$resolvedInstance[$name] = static::$container->$name;
+ }
+
+ /**
+ * Clear a resolved facade instance.
+ *
+ * @param string $name
+ * @return void
+ */
+ public static function clearResolvedInstance($name)
+ {
+ unset(static::$resolvedInstance[$name]);
+ }
+
+ /**
+ * Clear all of the resolved instances.
+ *
+ * @return void
+ */
+ public static function clearResolvedInstances()
+ {
+ static::$resolvedInstance = [];
+ }
+
+ /**
+ * Get the container instance behind the facade.
+ *
+ * @return \Interop\Container\ContainerInterface
+ */
+ public static function getFacadeContainer()
+ {
+ return static::$container;
+ }
+
+ /**
+ * Set the container instance.
+ *
+ * @param \Interop\Container\ContainerInterface $container
+ * @return void
+ */
+ public static function setFacadeContainer($container)
+ {
+ static::$container = $container;
+ }
+
+ /**
+ * Handle dynamic, static calls to the object.
+ *
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ *
+ * @throws \RuntimeException
+ */
+ public static function __callStatic($method, $args)
+ {
+ $instance = static::getFacadeRoot();
+
+ if (! $instance) {
+ throw new RuntimeException('A facade root has not been set.');
+ }
+
+ switch (count($args)) {
+ case 0:
+ return $instance->$method();
+ case 1:
+ return $instance->$method($args[0]);
+ case 2:
+ return $instance->$method($args[0], $args[1]);
+ case 3:
+ return $instance->$method($args[0], $args[1], $args[2]);
+ case 4:
+ return $instance->$method($args[0], $args[1], $args[2], $args[3]);
+ default:
+ return call_user_func_array([$instance, $method], $args);
+ }
+ }
+}
diff --git a/main/app/system/ServicesProvider.php b/main/app/system/ServicesProvider.php
new file mode 100755
index 0000000..6286bc0
--- /dev/null
+++ b/main/app/system/ServicesProvider.php
@@ -0,0 +1,104 @@
+addPath('build', '', \UserFrosting\BUILD_DIR_NAME);
+ $locator->addPath('log', '', \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\LOG_DIR_NAME);
+ $locator->addPath('cache', '', \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\CACHE_DIR_NAME);
+ $locator->addPath('session', '', \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\SESSION_DIR_NAME);
+
+ // Use locator to initialize streams
+ ReadOnlyStream::setLocator($locator);
+
+ // Fire up StreamBuilder
+ $c->streamBuilder;
+
+ return $locator;
+ };
+
+ /**
+ * StreamBuilder, to fire up our custom StreamWrapper defined in the locator service.
+ */
+ $container['streamBuilder'] = function ($c) {
+
+ $streams = [
+ 'build' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream',
+ 'log' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream',
+ 'cache' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream',
+ 'session' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream',
+ 'sprinkles' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'assets' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'schema' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'templates' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'extra' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'locale' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'config' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'routes' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream',
+ 'factories' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream'
+ ];
+
+ // Before registering them, we need to unregister any that where previously registered.
+ // This will cause error when two scripts are run in succession from the CLI
+ foreach ($streams as $scheme => $handler) {
+ if (in_array($scheme, stream_get_wrappers())) {
+ stream_wrapper_unregister($scheme);
+ }
+ }
+
+ $sb = new StreamBuilder($streams);
+
+ return $sb;
+ };
+
+ /**
+ * Set up sprinkle manager service.
+ */
+ $container['sprinkleManager'] = function ($c) {
+ $sprinkleManager = new SprinkleManager($c);
+ return $sprinkleManager;
+ };
+ }
+}
diff --git a/main/app/system/SlimAppEvent.php b/main/app/system/SlimAppEvent.php
new file mode 100755
index 0000000..f1217a5
--- /dev/null
+++ b/main/app/system/SlimAppEvent.php
@@ -0,0 +1,29 @@
+app = $app;
+ }
+
+ public function getApp()
+ {
+ return $this->app;
+ }
+}
diff --git a/main/app/system/Sprinkle/Sprinkle.php b/main/app/system/Sprinkle/Sprinkle.php
new file mode 100755
index 0000000..4707025
--- /dev/null
+++ b/main/app/system/Sprinkle/Sprinkle.php
@@ -0,0 +1,56 @@
+ci = $ci;
+ }
+}
diff --git a/main/app/system/Sprinkle/SprinkleManager.php b/main/app/system/Sprinkle/SprinkleManager.php
new file mode 100755
index 0000000..c206cea
--- /dev/null
+++ b/main/app/system/Sprinkle/SprinkleManager.php
@@ -0,0 +1,236 @@
+ci = $ci;
+ $this->sprinklesPath = \UserFrosting\APP_DIR_NAME . \UserFrosting\DS . \UserFrosting\SPRINKLES_DIR_NAME . \UserFrosting\DS;
+
+ $this->resourcePaths = [
+ 'assets' => \UserFrosting\DS . \UserFrosting\ASSET_DIR_NAME,
+ 'config' => \UserFrosting\DS . \UserFrosting\CONFIG_DIR_NAME,
+ 'extra' => \UserFrosting\DS . \UserFrosting\EXTRA_DIR_NAME,
+ 'factories' => \UserFrosting\DS . \UserFrosting\FACTORY_DIR_NAME,
+ 'locale' => \UserFrosting\DS . \UserFrosting\LOCALE_DIR_NAME,
+ 'routes' => \UserFrosting\DS . \UserFrosting\ROUTE_DIR_NAME,
+ 'schema' => \UserFrosting\DS . \UserFrosting\SCHEMA_DIR_NAME,
+ 'sprinkles' => '',
+ 'templates' => \UserFrosting\DS . \UserFrosting\TEMPLATE_DIR_NAME
+ ];
+ }
+
+ /**
+ * Adds the relative path for a specified resource type in a Sprinkle to the resource's stream.
+ *
+ * @param string $resourceName
+ * @param string $sprinkleName
+ * @return string|bool The full path to specified resource for the specified Sprinkle (if found).
+ */
+ public function addResource($resourceName, $sprinkleName)
+ {
+ $resourcePath = $this->resourcePaths[$resourceName];
+ $fullPath = $this->sprinklesPath . $sprinkleName . $resourcePath;
+
+ $this->ci->locator->addPath($resourceName, '', $fullPath);
+
+ return $this->ci->locator->findResource("$resourceName://", true, false);
+
+ /* This would allow a stream to subnavigate to a specific sprinkle (e.g. "templates://core/")
+ Not sure if we need this.
+ $locator->addPath('templates', '$name', $sprinklesDirFragment . '/' . \UserFrosting\TEMPLATE_DIR_NAME);
+ */
+ }
+
+ /**
+ * Register resource streams for all base sprinkles.
+ */
+ public function addResources()
+ {
+ // For each sprinkle, register its resources and then run its initializer
+ foreach ($this->sprinkles as $sprinkleName => $sprinkle) {
+ $this->addResource('config', $sprinkleName);
+ $this->addResource('assets', $sprinkleName);
+ $this->addResource('extra', $sprinkleName);
+ $this->addResource('factories', $sprinkleName);
+ $this->addResource('locale', $sprinkleName);
+ $this->addResource('routes', $sprinkleName);
+ $this->addResource('schema', $sprinkleName);
+ $this->addResource('sprinkles', $sprinkleName);
+ $this->addResource('templates', $sprinkleName);
+ }
+ }
+
+ /**
+ * Takes the name of a Sprinkle, and creates an instance of the initializer object (if defined).
+ *
+ * Creates an object of a subclass of UserFrosting\System\Sprinkle\Sprinkle if defined for the sprinkle (converting to StudlyCase).
+ * Otherwise, returns null.
+ * @param $name The name of the Sprinkle to initialize.
+ */
+ public function bootSprinkle($name)
+ {
+ $className = Str::studly($name);
+ $fullClassName = "\\UserFrosting\\Sprinkle\\$className\\$className";
+
+ // Check that class exists. If not, set to null
+ if (class_exists($fullClassName)) {
+ $sprinkle = new $fullClassName($this->ci);
+ return $sprinkle;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a list of available sprinkle names.
+ *
+ * @return string[]
+ */
+ public function getSprinkleNames()
+ {
+ return array_keys($this->sprinkles);
+ }
+
+ /**
+ * Returns a list of available sprinkles.
+ *
+ * @return Sprinkle[]
+ */
+ public function getSprinkles()
+ {
+ return $this->sprinkles;
+ }
+
+ /**
+ * Initialize a list of Sprinkles, instantiating their boot classes (if they exist),
+ * and subscribing them to the event dispatcher.
+ *
+ * @param string[] $baseSprinkleNames
+ */
+ public function init($sprinkleNames)
+ {
+ foreach ($sprinkleNames as $sprinkleName) {
+ $sprinkle = $this->bootSprinkle($sprinkleName);
+
+ if ($sprinkle) {
+ // Subscribe the sprinkle to the event dispatcher
+ $this->ci->eventDispatcher->addSubscriber($sprinkle);
+ }
+
+ $this->sprinkles[$sprinkleName] = $sprinkle;
+ }
+ }
+
+ /**
+ * Initialize all base sprinkles in a specified Sprinkles schema file (e.g. 'sprinkles.json').
+ *
+ * @param string $schemaPath
+ */
+ public function initFromSchema($schemaPath)
+ {
+ $baseSprinkleNames = $this->loadSchema($schemaPath)->base;
+ $this->init($baseSprinkleNames);
+ }
+
+ /**
+ * Return if a Sprinkle is available
+ * Can be used by other Sprinkles to test if their dependencies are met
+ *
+ * @param $name The name of the Sprinkle
+ */
+ public function isAvailable($name)
+ {
+ return in_array($name, $this->getSprinkleNames());
+ }
+
+
+ /**
+ * Interate through the list of loaded Sprinkles, and invoke their ServiceProvider classes.
+ */
+ public function registerAllServices()
+ {
+ foreach ($this->getSprinkleNames() as $sprinkleName) {
+ $this->registerServices($sprinkleName);
+ }
+ }
+
+ /**
+ * Register services for a specified Sprinkle.
+ */
+ public function registerServices($name)
+ {
+ $className = Str::studly($name);
+ $fullClassName = "\\UserFrosting\\Sprinkle\\$className\\ServicesProvider\\ServicesProvider";
+
+ // Check that class exists, and register services
+ if (class_exists($fullClassName)) {
+ // Register core services
+ $serviceProvider = new $fullClassName();
+ $serviceProvider->register($this->ci);
+ }
+ }
+
+ /**
+ * Load list of Sprinkles from a JSON schema file (e.g. 'sprinkles.json').
+ *
+ * @param string $schemaPath
+ * @return string[]
+ */
+ protected function loadSchema($schemaPath)
+ {
+ $sprinklesFile = @file_get_contents($schemaPath);
+
+ if ($sprinklesFile === false) {
+ $errorMessage = "Error: Unable to determine Sprinkle load order. File '$schemaPath' not found or unable to read. Please create a 'sprinkles.json' file and try again.";
+ throw new FileNotFoundException($errorMessage);
+ }
+
+ return json_decode($sprinklesFile);
+ }
+}
diff --git a/main/app/system/UserFrosting.php b/main/app/system/UserFrosting.php
new file mode 100755
index 0000000..4f569ec
--- /dev/null
+++ b/main/app/system/UserFrosting.php
@@ -0,0 +1,187 @@
+ci = new Container;
+
+ // Set up facade reference to container.
+ Facade::setFacadeContainer($this->ci);
+ }
+
+ /**
+ * Fires an event with optional parameters.
+ *
+ * @param string $eventName
+ * @param Event $event
+ *
+ * @return Event
+ */
+ public function fireEvent($eventName, Event $event = null)
+ {
+ /** @var EventDispatcher $events */
+ $eventDispatcher = $this->ci->eventDispatcher;
+
+ return $eventDispatcher->dispatch($eventName, $event);
+ }
+
+ /**
+ * Return the underlying Slim App instance, if available.
+ *
+ * @return Slim\App
+ */
+ public function getApp()
+ {
+ return $this->app;
+ }
+
+ /**
+ * Return the DI container.
+ *
+ * @return Slim\Container
+ */
+ public function getContainer()
+ {
+ return $this->ci;
+ }
+
+ /**
+ * Include all defined routes in route stream.
+ *
+ * Include them in reverse order to allow higher priority routes to override lower priority.
+ */
+ public function loadRoutes()
+ {
+ // Since routes aren't encapsulated in a class yet, we need this workaround :(
+ global $app;
+ $app = $this->app;
+
+ $routePaths = array_reverse($this->ci->locator->findResources('routes://', true, true));
+ foreach ($routePaths as $path) {
+ $routeFiles = glob($path . '/*.php');
+ foreach ($routeFiles as $routeFile) {
+ require_once $routeFile;
+ }
+ }
+ }
+
+ /**
+ * Initialize the application. Set up Sprinkles and the Slim app, define routes, register global middleware, and run Slim.
+ */
+ public function run()
+ {
+ $this->setupSprinkles();
+
+ // Set the configuration settings for Slim in the 'settings' service
+ $this->ci->settings = $this->ci->config['settings'];
+
+ // Next, we'll instantiate the Slim application. Note that the application is required for the SprinkleManager to set up routes.
+ $this->app = new App($this->ci);
+
+ $slimAppEvent = new SlimAppEvent($this->app);
+
+ $this->fireEvent('onAppInitialize', $slimAppEvent);
+
+ // Set up all routes
+ $this->loadRoutes();
+
+ // Add global middleware
+ $this->fireEvent('onAddGlobalMiddleware', $slimAppEvent);
+
+ $this->app->run();
+ }
+
+ /**
+ * Register system services, load all sprinkles, and add their resources and services.
+ *
+ * @param bool $isWeb Set to true if setting up in an HTTP/web environment, false if setting up for CLI scripts.
+ */
+ public function setupSprinkles($isWeb = true)
+ {
+ // Register system services
+ $serviceProvider = new ServicesProvider();
+ $serviceProvider->register($this->ci);
+
+ // Boot the Sprinkle manager, which creates Sprinkle classes and subscribes them to the event dispatcher
+ $sprinkleManager = $this->ci->sprinkleManager;
+
+ try {
+ $sprinkleManager->initFromSchema(\UserFrosting\SPRINKLES_SCHEMA_FILE);
+ } catch (FileNotFoundException $e) {
+ if ($isWeb) {
+ $this->renderSprinkleErrorPage($e->getMessage());
+ } else {
+ $this->renderSprinkleErrorCli($e->getMessage());
+ }
+ }
+
+ $this->fireEvent('onSprinklesInitialized');
+
+ // Add Sprinkle resources (assets, templates, etc) to locator
+ $sprinkleManager->addResources();
+ $this->fireEvent('onSprinklesAddResources');
+
+ // Register Sprinkle services
+ $sprinkleManager->registerAllServices();
+ $this->fireEvent('onSprinklesRegisterServices');
+ }
+
+ /**
+ * Render a basic error page for problems with loading Sprinkles.
+ */
+ protected function renderSprinkleErrorPage($errorMessage = "")
+ {
+ ob_clean();
+ $title = "UserFrosting Application Error";
+ $errorMessage = "Unable to start site. Contact owner.
" .
+ "Version: UserFrosting ".\UserFrosting\VERSION."
" .
+ $errorMessage;
+ $output = sprintf(
+ "
" .
+ "%s%s
%s",
+ $title,
+ $title,
+ $errorMessage
+ );
+ exit($output);
+ }
+
+ /**
+ * Render a CLI error message for problems with loading Sprinkles.
+ */
+ protected function renderSprinkleErrorCli($errorMessage = "")
+ {
+ exit($errorMessage . PHP_EOL);
+ }
+}
--
cgit v1.2.3