1
0
Fork 0
mirror of https://github.com/Oreolek/kohana-migrations.git synced 2024-05-18 17:08:18 +03:00

Initial import

This commit is contained in:
Matt Button 2010-12-24 15:52:03 +00:00
commit ed98d5b7c8
14 changed files with 723 additions and 0 deletions

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# Minion
Minion is a module for the Kohana framework which allows you to run various tasks from the cli.
## Getting Started
First off, download and enable the module in your bootstrap
Then you can run minion like so:
php index.php --uri=minion/{task}
To view a list of minion tasks, run
php index.php --uri=minion/help
To view help for a specific minion task run
php index.php --uri=minion/help/{task}
For security reasons Minion will only run from the cli. Attempting to access it over http will cause
a `Request_Exception` to be thrown.
## Writing your own tasks
All minion tasks must be located in `classes/minion/task/`. They can be in any module, thus allowing you to
ship custom minion tasks with your own module / product.
Each task must extend the abstract class `Minion_Task` and implement `Minion_Task::get_config_options()` and `Minion_Task::execute()`.
See `Minion_Task` for more details.
## Documentation
Code should be commented well enough not to need documentation, and minion can extract a class' doccomment to use
as documentation on the cli.
## Testing
This module is unittested using the [unittest module](http://github.com/kohana/unittest)

View file

@ -0,0 +1,114 @@
<?php
/**
* Controller for interacting with minion on the cli
*
* @author Matt Button <matthew@sigswitch.com>
**/
class Controller_Minion extends Controller
{
/**
* Prevent Minion from being run over http
*/
public function before()
{
if( ! Kohana::$is_cli)
{
throw new Request_Exception("Minion can only be ran from the cli");
}
return parent::before();
}
/**
* Prints out the help for a specific task
*
*/
public function action_help()
{
$tasks = Minion_Util::compile_task_list(Kohana::list_files('classes/minion/task'));
$task = $this->request->param('task');
$view = NULL;
if(empty($task))
{
$view = new View('minion/help/list');
$view->tasks = $tasks;
}
else
{
$class = Minion_Util::convert_task_to_class_name($task);
if( ! class_exists($class))
{
echo View::factory('minion/help/error')
->set('error', 'Task "'.$task.'" does not exist');
exit(1);
}
$inspector = new ReflectionClass($class);
list($description, $tags) = Minion_Util::parse_doccomment($inspector->getDocComment());
$view = View::factory('minion/help/task')
->set('description', $description)
->set('tags', (array) $tags)
->set('task', $task);
}
echo $view;
}
/**
* Handles the request to execute a task.
*
* Responsible for parsing the tasks to execute & also any config items that
* should be passed to the tasks
*/
public function action_execute()
{
$tasks = trim($this->request->param('task'));
if(empty($tasks))
return $this->action_help();
$tasks = explode(',', $tasks);
$master = new Minion_Master;
$options = $master->load($tasks)->get_config_options();
$config = array();
// Allow the user to specify config for each task, namespacing each
// config option with the name of the task that "owns" it
foreach($options as $task_name => $task_options)
{
$namespace = $task_name.Minion_Util::$task_separator;
// Namespace each config option
foreach($task_options as $i => $task_option)
{
$task_options[$i] = $namespace.$task_option;
}
// Get any config options the user's passed
$task_config = call_user_func_array(array('CLI', 'options'), $task_options);
if( ! empty($task_config))
{
$namespace_length = strlen($namespace);
// Strip the namespace off all the config options
foreach($task_config as $key => $value)
{
$config[$task_name][substr($key, $namespace_length)] = $value;
}
}
}
$master->execute($config);
}
}

93
classes/minion/master.php Normal file
View file

@ -0,0 +1,93 @@
<?php
/**
* The Minion Master is responsible for loading and executing the various minion
* tasks requested by the user
*
* @author Matt Button <matthew@sigswitch.com>
*/
class Minion_Master {
/**
* Tasks the master will execute
* @var array
*/
protected $_tasks = array();
/**
* Get a list of config options that the loaded tasks accept at execution
*
* @return array
*/
public function get_config_options()
{
$config = array();
foreach($this->_tasks as $task)
{
$config[(string) $task] = (array) $task->get_config_options();
}
return $config;
}
/**
* Loads a number of tasks into the task master
*
* Passed task can either be an instance of Minion_Task, a task name (e.g.
* db:migrate) or an array of the above
*
* If an invalid task is passed then a Kohana_Exception will be thrown
*
* @chainable
* @throws Kohana_Exception
* @param array|string|Minion_Task The task(s) to load
* @returns Minion_Master Chainable instance
*/
public function load($task)
{
if(is_array($task))
{
array_map(array($this, 'load'), $task);
return $this;
}
if(is_string($task))
{
$class = Minion_Util::convert_task_to_class_name($task);
$task = new $class;
}
if( ! $task instanceof Minion_Task)
{
throw new Kohana_Exception(
"Task ':task' is not a valid minion task",
array(':task' => get_class($task))
);
}
$this->_tasks[(string) $task] = $task;
return $this;
}
/**
* Executes the loaded tasks one at a time
*
* @return Minion_Master Chainable instance
*/
public function execute(array $config = array())
{
if(empty($this->_tasks))
return $this;
foreach($this->_tasks as $task)
{
$task->execute(Arr::get($config, (string) $task, array()));
}
return $this;
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* The base migration class, must be extended by all migration files
*
* Each migration file must implement an up() and a down() which are used to
* apply / remove this migration from the schema respectively
*
* @author Matt Button <matthew@sigswitch.com>
*/
abstract class Minion_Migration_Base {
/**
* Runs any SQL queries necessary to bring the database up a migration version
*
*/
abstract public function up();
/**
* Runs any SQL queries necessary to bring the database schema down a version
*
*/
abstract public function down();
}

39
classes/minion/task.php Normal file
View file

@ -0,0 +1,39 @@
<?php
/**
* Interface that all minion tasks must implement
*
*/
abstract class Minion_Task {
/**
* Gets the task name for the task
*
* @return string
*/
public function __toString()
{
static $task_name = NULL;
if($task_name === NULL)
{
$task_name = Minion_Util::convert_class_to_task($this);
}
return $task_name;
}
/**
* Get a set of config options that this task can accept
*
* @return array
*/
abstract public function get_config_options();
/**
* Execute the task with the specified set of config
*
* @return boolean TRUE if task executed successfully, else FALSE
*/
abstract public function execute(array $config);
}

28
classes/minion/task/app/cache/purge.php vendored Normal file
View file

@ -0,0 +1,28 @@
<?php
/**
* Purges the application file cache
*
* @author Matt Button <matthew@sigswitch.com>
**/
class Minion_Task_Cache_Purge extends Minion_Task
{
/**
* Gets a set of config options this minion task accepts
*
* @return array
*/
public function get_config_options()
{
return array();
}
/**
* Clears the cache
*/
public function execute(array $config)
{
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* The Migrate task compares the current version of the database with the target
* version and then executes the necessary commands to bring the database up to
* date
*
* Available config options are:
*
* db:migrate:version=version
*
* The version to which the database should be migrated. If this is NULL then
* it will be updated to the latest available version
*
* db:migrate:modules=module[,module2[,module3...]]
*
* A list of modules that will be used to source migration files. By default
* migrations will be loaded from all enabled modules
*
* @author Matt Button <matthew@sigswitch.com>
*/
class Minion_Task_Db_Migrate extends Minion_Task
{
/**
* Get a set of config options that migrations will accept
*
* @return array
*/
public function get_config_options()
{
return array(
'version',
'modules',
);
}
/**
* Migrates the database to the version specified
*
* @param array Configuration to use
*/
public function execute(array $config)
{
$k_config = Kohana::config('minion/task/migrations');
// Default is upgrade to latest
$version = Arr::get($config, 'version', NULL);
// Do fancy migration stuff here
}
}

124
classes/minion/util.php Normal file
View file

@ -0,0 +1,124 @@
<?php
/**
* Utility class for Minion
**/
class Minion_Util
{
/**
* The separator used to separate different levels of tasks
* @var string
*/
public static $task_separator = ':';
/**
* Parses a doccomment, extracting both the comment and any tags associated
*
* Based on the code in Kodoc::parse()
*
* @param string The comment to parse
* @return array First element is the comment, second is an array of tags
*/
public static function parse_doccomment($comment)
{
// Normalize all new lines to \n
$comment = str_replace(array("\r\n", "\n"), "\n", $comment);
// Remove the phpdoc open/close tags and split
$comment = array_slice(explode("\n", $comment), 1, -1);
// Tag content
$tags = array();
foreach ($comment as $i => $line)
{
// Remove all leading whitespace
$line = preg_replace('/^\s*\* ?/m', '', $line);
// Search this line for a tag
if (preg_match('/^@(\S+)(?:\s*(.+))?$/', $line, $matches))
{
// This is a tag line
unset($comment[$i]);
$name = $matches[1];
$text = isset($matches[2]) ? $matches[2] : '';
$tags[$name] = $text;
}
else
{
$comment[$i] = (string) $line;
}
}
$comment = trim(implode("\n", $comment));
return array($comment, $tags);
}
/**
* Compiles a list of available tasks from a directory structure
*
* @param array Directory structure of tasks
* @return array Compiled tasks
*/
public static function compile_task_list(array $files, $prefix = '')
{
$output = array();
foreach($files as $file => $path)
{
$file = substr($file, strrpos($file, '/') + 1);
if(is_array($path) AND count($path))
{
$task = Minion_Util::compile_task_list($path, $prefix.$file.Minion_Util::$task_separator);
if($task)
{
$output = array_merge($output, $task);
}
}
else
{
$output[] = strtolower($prefix.substr($file, 0, -strlen(EXT)));
}
}
return $output;
}
/**
* Converts a task (e.g. db:migrate to a class name)
*
* @param string Task name
* @return string Class name
*/
public static function convert_task_to_class_name($task)
{
$task = trim($task);
if(empty($task))
return '';
return 'Minion_Task_'.implode('_', array_map('ucfirst', explode(Minion_Util::$task_separator, $task)));
}
/**
* Gets the task name of a task class / task object
*
* @param string|Minion_Task The task class / object
* @return string The task name
*/
public static function convert_class_to_task($class)
{
if(is_object($class))
{
$class = get_class($class);
}
return strtolower(str_replace('_', Minion_Util::$task_separator, substr($class, 12)));
}
}

8
init.php Normal file
View file

@ -0,0 +1,8 @@
<?php
Route::set('minion', 'minion(/<action>)(/<task>)', array('action' => 'help'))
->defaults(array(
'controller' => 'minion',
'action' => 'execute',
));

26
tests/minion/master.php Normal file
View file

@ -0,0 +1,26 @@
<?php
/**
* Test case for the Minion Master
*
* @group minion
**/
class Minion_MasterTest extends Kohana_Unittest_TestCase
{
/**
* Tests that Minion_Master::load() will accept an instance of Minion_Task
* as a task
*
* @test
* @covers Minion_Master::load
*/
public function test_load_accepts_objects_as_valid_tasks()
{
$master = new Minion_Master;
$task = $this->getMockForAbstractClass('Minion_Task');
$this->assertSame($master, $master->load($task));
$this->assertAttributeContains($task, '_tasks', $master);
}
}

136
tests/minion/util.php Normal file
View file

@ -0,0 +1,136 @@
<?php
/**
* Test case for Minion_Util
*
* @group minion
**/
class Minion_UtilTest extends Kohana_Unittest_TestCase
{
/**
* Provides test data for test_parse_doccoment()
*
* @return array Test data
*/
public function provider_parse_doccoment()
{
return array(
array(
array(
"This is my comment from something or\nother",
array(
'author' => 'Matt Button <matthew@sigswitch.com>',
),
),
" /**\n * This is my comment from something or\n * other\n * \n * @author Matt Button <matthew@sigswitch.com>\n */",
),
);
}
/**
* Tests Minion_Util::prase_doccoment
*
* @test
* @dataProvider provider_parse_doccoment
* @covers Minion_Util::parse_doccomment
* @param array Expected output
* @param string Input doccoment
*/
public function test_parse_doccoment($expected, $doccomment)
{
$this->assertSame($expected, Minion_Util::parse_doccomment($doccomment));
}
/**
* Provides test data for test_compile_task_list()
*
* @return array Test data
*/
public function provider_compile_task_list()
{
return array(
array(
array(
'db:migrate',
'db:status',
),
array (
'classes/minion/task/db' => array (
'classes/minion/task/db/migrate.php' => '/var/www/memberful/memberful-core/modules/kohana-minion/classes/minion/task/db/migrate.php',
'classes/minion/task/db/status.php' => '/var/www/memberful/memberful-core/modules/kohana-minion/classes/minion/task/db/status.php',
),
),
),
);
}
/**
* Tests that compile_task_list accurately creates a list of tasks from a directory structure
*
* @test
* @covers Minion_Util::compile_task_list
* @dataProvider provider_compile_task_list
* @param array Expected output
* @param array List of files
* @param string Prefix to use
* @param string Separator to use
*/
public function test_compile_task_list($expected, $files, $prefix = '', $separator = ':')
{
$this->assertSame($expected, Minion_Util::compile_task_list($files, $prefix, $separator));
}
/**
* Provides test data for test_convert_task_to_class_name()
*
* @return array
*/
public function provider_convert_task_to_class_name()
{
return array(
array('Minion_Task_Db_Migrate', 'db:migrate'),
array('Minion_Task_Db_Status', 'db:status'),
array('', ''),
);
}
/**
* Tests that a task can be converted to a class name
*
* @test
* @covers Minion_Util::convert_task_to_class_name
* @dataProvider provider_convert_task_to_class_name
* @param string Expected class name
* @param string Input task name
*/
public function test_convert_task_to_class_name($expected, $task_name)
{
$this->assertSame($expected, Minion_Util::convert_task_to_class_name($task_name));
}
/**
* Provides test data for test_convert_class_to_task()
*
* @return array
*/
public function provider_convert_class_to_task()
{
return array(
array('db:migrate', 'Minion_Task_Db_Migrate'),
);
}
/**
* Tests that the task name can be found from a class name / object
*
* @test
* @covers Minion_Util::convert_class_to_task
* @dataProvider provider_convert_class_to_task
* @param string Expected task name
* @param mixed Input class
*/
public function test_convert_class_to_task($expected, $class)
{
$this->assertSame($expected, Minion_Util::convert_class_to_task($class));
}
}

View file

@ -0,0 +1,7 @@
<?php echo $error; ?>
Run
index.php --uri=minion
for more help

View file

@ -0,0 +1,17 @@
Minion is a cli tool for performing tasks
Usage
php index.php --uri=minion/{task}
Where {task} is one of the following:
<?php foreach($tasks as $task): ?>
* <?php echo $task; ?>
<?php endforeach; ?>
For more information on what a task does and usage details execute
php index.php --uri=minion/help/{task}

View file

@ -0,0 +1,17 @@
Usage
=======
php index.php --uri=minion/<?php echo $task; ?> [--option1=value1] [--option2=value2]
Details
=======
<?php foreach($tags as $tag_name => $tag_content): ?>
<?php echo ucfirst($tag_name) ?>: <?php echo $tag_content ?>
<?php endforeach; ?>
Description
===========
<?php echo $description; ?>