merged skunkworks into develop.

This commit is contained in:
Taylor Otwell
2012-01-16 13:59:24 -06:00
parent 610d8827c4
commit b5442c67fc
117 changed files with 7268 additions and 3999 deletions

62
laravel/cli/artisan.php Normal file
View File

@@ -0,0 +1,62 @@
<?php namespace Laravel\CLI; defined('APP_PATH') or die('No direct script access.');
use Laravel\IoC;
use Laravel\Bundle;
use Laravel\Database as DB;
/**
* Fire up the default bundle. This will ensure any dependencies that
* need to be registered in the IoC container are registered and that
* the auto-loader mappings are registered.
*/
Bundle::start(DEFAULT_BUNDLE);
/**
* We will register all of the Laravel provided tasks inside the IoC
* container so they can be resolved by the task class. This allows
* us to seamlessly add tasks to the CLI so that the Task class
* doesn't have to worry about how to resolve core tasks.
*/
/**
* The bundle task is responsible for the installation of bundles
* and their dependencies. It utilizes the bundles API to get the
* meta-data for the available bundles.
*/
IoC::register('task: bundle', function()
{
return new Tasks\Bundle\Bundler;
});
/**
* The migrate task is responsible for running database migrations
* as well as migration rollbacks. We will also create an instance
* of the migration resolver and database classes, which are used
* to perform various support functions for the migrator.
*/
IoC::register('task: migrate', function()
{
$database = new Tasks\Migrate\Database;
$resolver = new Tasks\Migrate\Resolver($database);
return new Tasks\Migrate\Migrator($resolver, $database);
});
/**
* We will wrap the command execution in a try / catch block and
* simply write out any exception messages we receive to the CLI
* for the developer. Note that this only writes out messages
* for the CLI exceptions. All others will be not be caught
* and will be totally dumped out to the CLI.
*/
try
{
Command::run(array_slice($_SERVER['argv'], 1));
}
catch (\Exception $e)
{
echo $e->getMessage();
}
echo PHP_EOL;

109
laravel/cli/command.php Normal file
View File

@@ -0,0 +1,109 @@
<?php namespace Laravel\CLI;
use Laravel\IoC;
use Laravel\Str;
use Laravel\Bundle;
class Command {
/**
* Run a CLI task with the given arguments.
*
* @param array $arguments
* @return void
*/
public static function run($arguments = array())
{
if ( ! isset($arguments[0]))
{
throw new \Exception("Whoops! You forgot to provide the task name.");
}
list($bundle, $task, $method) = static::parse($arguments[0]);
// If the task exists within a bundle, we will start the bundle so that
// any dependencies can be registered in the application IoC container.
// If the task is registered in the container, it will be resolved
// via the container instead of by this class.
if (Bundle::exists($bundle)) Bundle::start($bundle);
if (is_null($task = static::resolve($bundle, $task)))
{
throw new \Exception("Sorry, I can't find that task.");
}
$task->$method(array_slice($arguments, 1));
}
/**
* Parse the task name to extract the bundle, task, and method.
*
* @param string $task
* @return array
*/
protected static function parse($task)
{
list($bundle, $task) = Bundle::parse($task);
// Extract the task method from the task string. Methods are called
// on tasks by separating the task and method with a single colon.
// If no task is specified, "run" is used as the default method.
if (str_contains($task, ':'))
{
list($task, $method) = explode(':', $task);
}
else
{
$method = 'run';
}
return array($bundle, $task, $method);
}
/**
* Resolve an instance of the given task name.
*
* @param string $bundle
* @param string $task
* @return object
*/
public static function resolve($bundle, $task)
{
$identifier = Bundle::identifier($bundle, $task);
// First we'll check to see if the task has been registered in
// the application IoC container. This allows dependencies to
// be injected into tasks for more testability.
if (IoC::registered("task: {$identifier}"))
{
return IoC::resolve("task: {$identifier}");
}
// If the task file exists, we'll format the bundle and task
// name into a task class name and resolve an instance of
// the so that the requested method may be executed.
if (file_exists($path = Bundle::path($bundle).'tasks/'.$task.EXT))
{
require $path;
$task = static::format($bundle, $task);
return new $task;
}
}
/**
* Format a bundle and task into a task class name.
*
* @param string $bundle
* @param string $task
* @return string
*/
protected static function format($bundle, $task)
{
$prefix = Bundle::class_prefix($bundle);
return '\\'.$prefix.Str::clasify($task).'_Task';
}
}

View File

@@ -0,0 +1,127 @@
<?php namespace Laravel\CLI\Tasks\Bundle; defined('APP_PATH') or die('No direct script access.');
use Laravel\IoC;
use Laravel\Bundle;
use Laravel\CLI\Tasks\Task;
IoC::singleton('bundle.repository', function()
{
return new Repository;
});
IoC::singleton('bundle.publisher', function()
{
return new Publisher;
});
IoC::singleton('bundle.provider: github', function()
{
return new Providers\Github;
});
class Bundler extends Task {
/**
* Install the given bundles into the application.
*
* @param array $bundles
* @return void
*/
public function install($bundles)
{
$publisher = IoC::resolve('bundle.publisher');
foreach ($this->get($bundles) as $bundle)
{
if (is_dir(BUNDLE_PATH.$bundle['name']))
{
echo "Bundle {$bundle['name']} is already installed.";
continue;
}
// Once we have the bundle information, we can resolve an instance
// of a provider and install the bundle into the application and
// all of its registered dependencies as well.
//
// Each bundle provider implements the Provider interface and
// is repsonsible for retrieving the bundle source from its
// hosting party and installing it into the application.
$provider = "bundle.provider: {$bundle['provider']}";
IoC::resolve($provider)->install($bundle);
$publisher->publish($bundle);
}
}
/**
* Publish bundle assets to the public directory.
*
* @param array $bundles
* @return void
*/
public function publish($bundles)
{
// If no bundles are passed to the command, we'll just gather all
// of the installed bundle names and publish the assets for each
// for each one of the bundles to the public directory.
if (count($bundles) == 0) $bundles = Bundle::all();
$publisher = IoC::resolve('bundle.publisher');
foreach ($bundles as $bundle)
{
$publisher->publish($bundle);
}
}
/**
* Gather all of the bundles from the bundle repository.
*
* @param array $bundles
* @return array
*/
protected function get($bundles)
{
$responses = array();
$repository = IoC::resolve('bundle.repository');
// This method is primarily responsible for gathering the data
// for all bundles that need to be installed. This allows us
// to verify the existence of the bundle before even getting
// started on the actual installation process.
foreach ($bundles as $bundle)
{
// First we'll call the bundle repository to gather the bundle data
// array, which contains all of the information needed to install
// the bundle into the application. We'll verify that the bundle
// exists and the API is responding for each bundle.
$response = $repository->get($bundle);
if ( ! $response)
{
throw new \Exception("The bundle API is not responding.");
}
if ($response['status'] == 'not-found')
{
throw new \Exception("There is not a bundle named [$bundle].");
}
// If the bundle was retrieved successfully, we will add it to
// our array of bundles, as well as merge all of the bundle's
// dependencies into the array of responses so that they are
// installed along with the consuming dependency.
$bundle = $response['bundle'];
$responses[] = $bundle;
$responses = array_merge($responses, $this->get($bundle['dependencies']));
}
return $responses;
}
}

View File

@@ -0,0 +1,27 @@
<?php namespace Laravel\CLI\Tasks\Bundle\Providers;
class Github implements Provider {
/**
* Install the given bundle into the application.
*
* @param string $bundle
* @return void
*/
public function install($bundle)
{
$repository = "git://github.com/{$bundle['location']}.git";
// We need to just extract the basename of the bundle path when
// adding the submodule. Of course, we can't add a submodule to
// a location outside of the Git repository, so we don't need
// the full bundle path. We'll just take the basename in case
// the bundle directory has been renamed.
$path = basename(BUNDLE_PATH).'/';
passthru('git submodule add '.$repository.' '.$path.$bundle['name']);
passthru('git submodule update');
}
}

View File

@@ -0,0 +1,13 @@
<?php namespace Laravel\CLI\Tasks\Bundle\Providers;
interface Provider {
/**
* Install the given bundle into the application.
*
* @param string $bundle
* @return void
*/
public function install($bundle);
}

View File

@@ -0,0 +1,90 @@
<?php namespace Laravel\CLI\Tasks\Bundle;
use Laravel\Bundle;
use FilesystemIterator;
class Publisher {
/**
* Publish a bundle's assets to the public directory.
*
* @param string $bundle
* @return void
*/
public function publish($bundle)
{
$this->move($bundle, $this->from($bundle), $this->to($bundle));
echo "Assets published for bundle [$bundle].".PHP_EOL;
}
/**
* Copy the contents of a bundle's assets to the public folder.
*
* @param string $bundle
* @param string $source
* @param string $destination
* @return void
*/
protected function move($bundle, $source, $destination)
{
if ( ! is_dir($source)) return;
// First we need to create the destination directory if it doesn't
// already exists. This directory hosts all of the assets we copy
// from the installed bundle's source directory.
if ( ! is_dir($destination))
{
mkdir($destination);
}
$items = new FilesystemIterator($source, FilesystemIterator::SKIP_DOTS);
foreach ($items as $item)
{
// If the file system item is a directory, we will recurse the
// function, passing in the item directory. To get the proper
// destination path, we'll replace the root bundle asset
// directory with the root public asset directory.
if ($item->isDir())
{
$path = $item->getRealPath();
$recurse = str_replace($this->from($bundle), $this->to($bundle), $path);
$this->move($bundle, $path, $recurse);
}
// If the file system item is an actual file, we can copy the
// file from the bundle asset directory to the public asset
// directory. The "copy" method will overwrite any existing
// files with the same name.
else
{
copy($item->getRealPath(), $destination.DS.$item->getBasename());
}
}
}
/**
* Get the "to" location of the bundle's assets.
*
* @param string $bundle
* @return string
*/
protected function to($bundle)
{
return PUBLIC_PATH.'bundles'.DS.$bundle.DS;
}
/**
* Get the "from" location of the bundle's assets.
*
* @param string $bundle
* @return string
*/
protected function from($bundle)
{
return Bundle::path($bundle).'public';
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace Laravel\CLI\Tasks\Bundle;
class Repository {
/**
* The root of the Laravel bundle API.
*
* @var string
*/
protected $api = 'http://bundles.laravel.com/api/';
/**
* Get the decoded JSON information for a bundle.
*
* @param string|int $bundle
* @return array
*/
public function get($bundle)
{
// The Bundle API will return a JSON string that we can decode and
// pass back to the consumer. The decoded array will contain info
// regarding the bundle's provider and location, as well as all
// of the bundle's dependencies.
$bundle = @file_get_contents($this->api.$bundle);
return json_decode($bundle, true);
}
}

View File

@@ -0,0 +1,83 @@
<?php namespace Laravel\CLI\Tasks\Migrate;
use Laravel\Database as DB;
class Database {
/**
* Log a migration in the migration table.
*
* @param string $bundle
* @param string $name
* @param int $batch
* @return void
*/
public function log($bundle, $name, $batch)
{
$this->table()->insert(compact('bundle', 'name', 'batch'));
}
/**
* Delete a row from the migration table.
*
* @param string $bundle
* @param string $name
* @return void
*/
public function delete($bundle, $name)
{
$this->table()->where_bundle_and_name($bundle, $name)->delete();
}
/**
* Return an array of the last batch of migrations.
*
* @return array
*/
public function last()
{
$table = $this->table();
// First we need to grab the last batch ID from the migration table,
// as this will allow us to grab the lastest batch of migrations
// that need to be run for a rollback command.
$id = $this->batch();
// Once we have the batch ID, we will pull all of the rows for that
// batch. Then we can feed the results into the resolve method to
// get the migration instances for the command.
return $table->where_batch($id)->order_by('name', 'desc')->get();
}
/**
* Get all of the migrations that have run for a bundle.
*
* @param string $bundle
* @return array
*/
public function ran($bundle)
{
return $this->table()->where_bundle($bundle)->lists('name');
}
/**
* Get the maximum batch ID from the migration table.
*
* @return int
*/
public function batch()
{
return $this->table()->max('batch');
}
/**
* Get a database query instance for the migration table.
*
* @return Query
*/
protected function table()
{
return DB::connection()->table('laravel_migrations');
}
}

View File

@@ -0,0 +1,235 @@
<?php namespace Laravel\CLI\Tasks\Migrate;
use Laravel\Str;
use Laravel\File;
use Laravel\Bundle;
use Laravel\CLI\Tasks\Task;
use Laravel\Database\Schema;
class Migrator extends Task {
/**
* The migration resolver instance.
*
* @var Resolver
*/
protected $resolver;
/**
* The migration database instance.
*
* @var Database
*/
protected $database;
/**
* Create a new instance of the Migrator CLI task.
*
* @param Resolver $resolver
* @param Database $database
* @return void
*/
public function __construct(Resolver $resolver, Database $database)
{
$this->resolver = $resolver;
$this->database = $database;
}
/**
* Run a database migration command.
*
* @param array $arguments
* @return void
*/
public function run($arguments = array())
{
// If no arguments were passed to the task, we will just migrate
// to the latest version across all bundles. Otherwise, we will
// parse the arguments to determine the bundle for which the
// database migrations should be run.
if (count($arguments) == 0)
{
$this->migrate();
}
else
{
$this->migrate(array_get($arguments, 0));
}
}
/**
* Run the outstanding migrations for a given bundle.
*
* @param string $bundle
* @param int $version
* @return void
*/
public function migrate($bundle = null, $version = null)
{
$migrations = $this->resolver->outstanding($bundle);
if (count($migrations) == 0)
{
echo "No outstanding migrations.";
return;
}
// We need to grab the latest batch ID and increment it
// by one. This allows us to group the migrations such
// that we can easily determine which migrations need
// to be rolled back for a given command.
$batch = $this->database->batch() + 1;
foreach ($migrations as $migration)
{
$migration['migration']->up();
echo 'Migrated: '.$this->display($migration).PHP_EOL;
// After running a migration, we log its execution in the
// migration table so that we can easily determine which
// migrations we will need to reverse on a rollback.
$this->database->log($migration['bundle'], $migration['name'], $batch);
}
}
/**
* Rollback the latest migration command.
*
* @param array $arguments
* @return bool
*/
public function rollback($arguments = array())
{
$migrations = $this->resolver->last();
if (count($migrations) == 0)
{
echo "Nothing to rollback.";
return false;
}
// The "last" method on the resolver returns an array of migrations,
// along with their bundles and names. We will iterate through each
// migration and run the "down" method, removing them from the
// database as we go.
foreach ($migrations as $migration)
{
$migration['migration']->down();
echo 'Rolled back: '.$this->display($migration).PHP_EOL;
// By only removing the migration after it has successfully rolled back,
// we can re-run the rollback command in the event of any errors with
// the migration. When we re-run, only the migrations that have not
// been rolled-back for the batch will still be in the database.
$this->database->delete($migration['bundle'], $migration['name']);
}
return true;
}
/**
* Rollback all of the executed migrations.
*
* @param array $arguments
* @return void
*/
public function reset($arguments = array())
{
while ($this->rollback()) {};
}
/**
* Install the database tables used by the migration system.
*
* @return void
*/
public function install()
{
Schema::table('laravel_migrations', function($table)
{
$table->create();
// Migrations can be run for a specific bundle, so we'll use
// the bundle name and string migration name as an unique ID
// for the migrations, allowing us to easily identify which
// migrations have been run for each bundle.
$table->string('bundle');
$table->string('name');
// When running a migration command, we will store a batch
// ID with each of the rows on the table. This will allow
// us to grab all of the migrations that were run for the
// last command when performing rollbacks.
$table->integer('batch');
$table->primary(array('bundle', 'name'));
});
echo "Migration table created successfully.";
}
/**
* Generate a new migration file.
*
* @param array $arguments
* @return void
*/
public function make($arguments = array())
{
if (count($arguments) == 0)
{
throw new \Exception("I need to know what to name the migration.");
}
list($bundle, $migration) = Bundle::parse($arguments[0]);
// The migration path is prefixed with the UNIX timestamp, which
// is a better way of ordering migrations than a simple integer
// incrementation, since developers may start working on the
// next migration at the same time unknowingly.
$date = date('Y_m_d').'_'.time();
$path = Bundle::path($bundle).'migrations/'.$date.'_'.$migration.EXT;
File::put($path, $this->stub($bundle, $migration));
echo "Great! New migration created!";
}
/**
* Get the stub migration with the proper class name.
*
* @param string $bundle
* @param string $migration
* @return string
*/
protected function stub($bundle, $migration)
{
$stub = File::get(SYS_PATH.'cli/tasks/migrate/stub'.EXT);
// The class name is formatted simialrly to tasks and controllers,
// where the bundle name is prefixed to the class if it is not in
// the default bundle. However, unlike tasks, there is nothing
// appended to the class name since they're already unique.
$class = Bundle::class_prefix($bundle).Str::classify($migration);
return str_replace('{{class}}', $class, $stub);
}
/**
* Get the migration bundle and name for display.
*
* @param array $migration
* @return string
*/
protected function display($migration)
{
return $migration['bundle'].'/'.$migration['name'];
}
}

View File

@@ -0,0 +1,159 @@
<?php namespace Laravel\CLI\Tasks\Migrate;
use Laravel\Bundle;
class Resolver {
/**
* The migration database instance.
*
* @var Database
*/
protected $database;
/**
* Create a new instance of the migration resolver.
*
* @param Database $datbase
* @return void
*/
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* Resolve all of the outstanding migrations for a bundle.
*
* @param string $bundle
* @return array
*/
public function outstanding($bundle = null)
{
$migrations = array();
// If no bundle was given to the command, we'll grab every bundle for
// the application, including the "application" bundle, which is not
// returned by "all" method on the Bundle class.
if (is_null($bundle))
{
$bundles = array_merge(Bundle::all(), array('application'));
}
else
{
$bundles = array($bundle);
}
foreach ($bundles as $bundle)
{
// First we need to grab all of the migrations that have already
// run for this bundle, as well as all of the migration files
// for the bundle. Once we have these, we can determine which
// migrations are still outstanding.
$ran = $this->database->ran($bundle);
$files = $this->migrations($bundle);
// To find outstanding migrations, we will simply iterate over
// the migration files and add the files that do not exist in
// the array of ran migrations to the outstanding array.
foreach ($files as $key => $name)
{
if ( ! in_array($name, $ran))
{
$migrations[] = compact('bundle', 'name');
}
}
}
return $this->resolve($migrations);
}
/**
* Resolve an array of the last batch of migrations.
*
* @return array
*/
public function last()
{
return $this->resolve($this->database->last());
}
/**
* Resolve an array of migration instances.
*
* @param array $migrations
* @return array
*/
protected function resolve($migrations)
{
$instances = array();
foreach ($migrations as $migration)
{
$migration = (array) $migration;
// The migration array contains the bundle name, so we will get the
// path to the bundle's migrations and resolve an instance of the
// migration using the name.
$bundle = $migration['bundle'];
$path = Bundle::path($bundle).'migrations/';
// Migrations are not resolved through the auto-loader, so we will
// manually instantiate the migration class instances for each of
// the migration names we're given.
$name = $migration['name'];
require_once $path.$name.EXT;
// Since the migration name will begin with the numeric ID, we'll
// slice off the ID so we are left with the migration class name.
// The IDs are for sorting when resolving outstanding migrations.
//
// Migrations that exist within bundles other than the default
// will be prefixed with the bundle name to avoid any possible
// naming collisions with other bundle's migrations.
$prefix = Bundle::class_prefix($bundle);
$class = $prefix.substr($name, 22);
$migration = new $class;
// When adding to the array of instances, we will actually
// add the migration instance, the bundle, and the name.
// This allows the migrator to log the bundle and name
// when the migration is executed.
$instances[] = compact('bundle', 'name', 'migration');
}
return $instances;
}
/**
* Grab all of the migration filenames for a bundle.
*
* @param string $bundle
* @return array
*/
protected function migrations($bundle)
{
$files = glob(Bundle::path($bundle).'migrations/*_*'.EXT);
// Once we have the array of files in the migration directory,
// we'll take the basename of the file and remove the PHP file
// extension, which isn't needed.
foreach ($files as &$file)
{
$file = str_replace(EXT, '', basename($file));
}
// We'll also sort the files so that the earlier migrations
// will be at the front of the array and will be resolved
// first by this class' resolve method.
sort($files);
return $files;
}
}

View File

@@ -0,0 +1,25 @@
<?php
class {{class}} {
/**
* Make changes to the database.
*
* @return void
*/
public function up()
{
//
}
/**
* Revert the changes to the database.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,3 @@
<?php namespace Laravel\CLI\Tasks;
abstract class Task {}