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

View File

@@ -9,13 +9,6 @@ class Connection {
*/
public $pdo;
/**
* All of the queries that have been executed on the connection.
*
* @var array
*/
public $queries = array();
/**
* The connection configuration array.
*
@@ -30,6 +23,13 @@ class Connection {
*/
protected $grammar;
/**
* All of the queries that have been executed on all connections.
*
* @var array
*/
public static $queries = array();
/**
* Create a new database connection instance.
*
@@ -46,6 +46,14 @@ class Connection {
/**
* Begin a fluent query against a table.
*
* <code>
* // Start a fluent query against the "users" table
* $query = DB::connection()->table('users');
*
* // Start a fluent query against the "users" table and get all the users
* $users = DB::connection()->table('users')->get();
* </code>
*
* @param string $table
* @return Query
*/
@@ -57,27 +65,22 @@ class Connection {
/**
* Create a new query grammar for the connection.
*
* Query grammars allow support for new database systems to be added quickly
* and easily. Since the responsibility of the query generation is delegated
* to the grammar classes, it is simple to override only the methods with
* SQL syntax that differs from the default implementation.
*
* @return Grammars\Grammar
* @return Query\Grammars\Grammar
*/
protected function grammar()
{
if (isset($this->grammar)) return $this->grammar;
// We allow the developer to hard-code a grammar for the connection. This really
// has no use yet; however, if database systems that can use multiple grammars
// like ODBC are added in the future, this will be needed.
switch (isset($this->config['grammar']) ? $this->config['grammar'] : $this->driver())
{
case 'mysql':
return $this->grammar = new Grammars\MySQL;
return $this->grammar = new Query\Grammars\MySQL;
case 'sqlsrv':
return $this->grammar = new Query\Grammars\SQLServer;
default:
return $this->grammar = new Grammars\Grammar;
return $this->grammar = new Query\Grammars\Grammar;
}
}
@@ -98,9 +101,9 @@ class Connection {
*/
public function only($sql, $bindings = array())
{
$result = (array) $this->first($sql, $bindings);
$results = (array) $this->first($sql, $bindings);
return reset($result);
return reset($results);
}
/**
@@ -127,67 +130,124 @@ class Connection {
}
/**
* Execute a SQL query against the connection.
*
* The method returns the following based on query type:
*
* SELECT -> Array of stdClasses
* UPDATE -> Number of rows affected.
* DELETE -> Number of Rows affected.
* ELSE -> Boolean true / false depending on success.
*
* <code>
* // Execute a query against the database connection
* $users = DB::connection()->query('select * from users');
*
* // Execute a query with bound parameters
* $user = DB::connection()->query('select * from users where id = ?', array($id));
* </code>
* Execute a SQL query and return an array of StdClass objects.
*
* @param string $sql
* @param array $bindings
* @return mixed
* @return array
*/
public function query($sql, $bindings = array())
{
// Since expressions are injected into the query as raw strings, we need
// to remove them from the array of bindings. They are not truly bound
// to the PDO statement as named parameters.
foreach ($bindings as $key => $value)
{
if ($value instanceof Expression) unset($bindings[$key]);
}
list($statement, $result) = $this->execute($sql, $bindings);
$bindings = array_values($bindings);
return $statement->fetchAll(PDO::FETCH_CLASS, 'stdClass');
}
/**
* Execute a SQL UPDATE query and return the affected row count.
*
* @param string $sql
* @param array $bindings
* @return int
*/
public function update($sql, $bindings = array())
{
list($statement, $result) = $this->execute($sql, $bindings);
return $statement->rowCount();
}
/**
* Execute a SQL DELETE query and return the affected row count.
*
* @param string $sql
* @param array $bindings
* @return int
*/
public function delete($sql, $bindings = array())
{
list($statement, $result) = $this->execute($sql, $bindings);
return $statement->rowCount();
}
/**
* Execute an SQL query and return the boolean result of the PDO statement.
*
* @param string $sql
* @param array $bindings
* @return bool
*/
public function statement($sql, $bindings = array())
{
list($statement, $result) = $this->execute($sql, $bindings);
return $result;
}
/**
* Execute a SQL query against the connection.
*
* The PDO statement and boolean result will be return in an array.
*
* @param string $sql
* @param array $bindings
* @return array
*/
protected function execute($sql, $bindings = array())
{
// Since expressions are injected into the query as strings, we need to
// remove them from the array of bindings. After we have removed them,
// we'll reset the array so there aren't gaps in the keys.
$bindings = array_values(array_filter($bindings, function($binding)
{
return ! $binding instanceof Expression;
}));
$sql = $this->transform($sql, $bindings);
$this->queries[] = compact('sql', 'bindings');
$statement = $this->pdo->prepare($sql);
return $this->execute($this->pdo->prepare($sql), $bindings);
// Every query is timed so that we can log the executinon time along
// with the query SQL and array of bindings. This should be make it
// convenient for the developer to profile the application's query
// performance to diagnose bottlenecks.
$time = microtime(true);
$result = $statement->execute($bindings);
$time = number_format((microtime(true) - $time) * 1000, 2);
// Once we have execute the query, we log the SQL, bindings, and
// execution time in a static array that is accessed by all of
// the connections used by the application. This allows us to
// review all of the executed SQL.
static::$queries[] = compact('sql', 'bindings', 'time');
return array($statement, $result);
}
/**
* Transform an SQL query into an executable query.
*
* Laravel provides a convenient short-cut when writing raw queries for
* handling cumbersome "where in" statements. This method will transform
* those segments into their full SQL counterparts.
*
* @param string $sql
* @param array $bindings
* @return string
*/
protected function transform($sql, $bindings)
{
// Laravel provides an easy short-cut notation for writing raw
// WHERE IN statements. If (...) is in the query, it will be
// replaced with the correct number of parameters based on
// the bindings for the query.
if (strpos($sql, '(...)') !== false)
{
for ($i = 0; $i < count($bindings); $i++)
{
// If the binding is an array, we can assume it is being used to fill
// a "where in" condition, so we will replace the next place-holder
// in the query with the correct number of parameters based on the
// number of elements in this binding.
// If the binding is an array, we can assume it is being used
// to fill a "where in" condition, so we'll replace the next
// place-holder in the SQL query with the correct number of
// parameters based on the elements in the binding.
if (is_array($bindings[$i]))
{
$parameters = implode(', ', array_fill(0, count($bindings[$i]), '?'));
@@ -200,33 +260,6 @@ class Connection {
return trim($sql);
}
/**
* Execute a prepared PDO statement and return the appropriate results.
*
* @param PDOStatement $statement
* @param array $bindings
* @return mixed
*/
protected function execute(PDOStatement $statement, $bindings)
{
$result = $statement->execute($bindings);
$sql = strtoupper($statement->queryString);
if (strpos($sql, 'SELECT') === 0)
{
return $statement->fetchAll(PDO::FETCH_CLASS, 'stdClass');
}
elseif (strpos($sql, 'UPDATE') === 0 or strpos($sql, 'DELETE') === 0)
{
return $statement->rowCount();
}
else
{
return $result;
}
}
/**
* Get the driver name for the database connection.
*

View File

@@ -16,7 +16,7 @@ abstract class Connector {
);
/**
* Establish a PDO database connection for a given database configuration.
* Establish a PDO database connection.
*
* @param array $config
* @return PDO
@@ -24,7 +24,7 @@ abstract class Connector {
abstract public function connect($config);
/**
* Get the PDO connection options for a given database configuration.
* Get the PDO connection options for the configuration.
*
* Developer specified options will override the default connection options.
*

View File

@@ -3,7 +3,7 @@
class MySQL extends Connector {
/**
* Establish a PDO database connection for a given database configuration.
* Establish a PDO database connection.
*
* @param array $config
* @return PDO
@@ -15,7 +15,7 @@ class MySQL extends Connector {
// Format the initial MySQL PDO connection string. These options are required
// for every MySQL connection that is established. The connection strings
// have the following convention: "mysql:host=hostname;dbname=database"
$dsn = sprintf('%s:host=%s;dbname=%s', $driver, $host, $database);
$dsn = "mysql:host={$host};dbname={$database}";
// Check for any optional MySQL PDO options. These options are not required
// to establish a PDO connection; however, may be needed in certain server

View File

@@ -3,7 +3,7 @@
class Postgres extends Connector {
/**
* Establish a PDO database connection for a given database configuration.
* Establish a PDO database connection.
*
* @param array $config
* @return PDO
@@ -15,7 +15,7 @@ class Postgres extends Connector {
// Format the initial Postgres PDO connection string. These options are required
// for every Postgres connection that is established. The connection strings
// have the following convention: "pgsql:host=hostname;dbname=database"
$dsn = sprintf('%s:host=%s;dbname=%s', $driver, $host, $database);
$dsn = "pgsql:host={$host};dbname={$database}";
// Check for any optional Postgres PDO options. These options are not required
// to establish a PDO connection; however, may be needed in certain server

View File

@@ -3,25 +3,7 @@
class SQLite extends Connector {
/**
* The path to the SQLite databases for the application.
*
* @var string
*/
protected $path;
/**
* Create a new SQLite database connector instance.
*
* @param string $path
* @return void
*/
public function __construct($path)
{
$this->path = $path;
}
/**
* Establish a PDO database connection for a given database configuration.
* Establish a PDO database connection.
*
* @param array $config
* @return PDO
@@ -32,27 +14,19 @@ class SQLite extends Connector {
// SQLite provides supported for "in-memory" databases, which exist only for the
// lifetime of the request. Any given in-memory database may only have one PDO
// connection open to it at a time. Generally, these databases are use for
// connection open to it at a time. Generally, these databases are used for
// testing and development purposes, not in production scenarios.
if ($config['database'] == ':memory:')
{
return new PDO('sqlite::memory:', null, null, $options);
}
// First, we will check for the database in the default storage directory for the
// application. If we don't find the database there, we will assume the database
// name is actually a full qualified path to the database on disk and attempt
// to load it. If we still can't find it, we'll bail out.
elseif (file_exists($path = $this->path.$config['database'].'.sqlite'))
if (file_exists($path = DATABASE_PATH.$config['database'].'.sqlite'))
{
return new PDO('sqlite:'.$path, null, null, $options);
}
elseif (file_exists($config['database']))
{
return new PDO('sqlite:'.$config['database'], null, null, $options);
}
throw new \OutOfBoundsException("SQLite database [{$config['database']}] could not be found.");
throw new \Exception("SQLite database [{$config['database']}] could not be found.");
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Laravel\Database\Connectors; use PDO;
class SQLServer extends Connector {
/**
* The PDO connection options.
*
* @var array
*/
protected $options = array(
PDO::ATTR_CASE => PDO::CASE_LOWER,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
);
/**
* Establish a PDO database connection.
*
* @param array $config
* @return PDO
*/
public function connect($config)
{
extract($config);
// Format the SQL Server connection string. This connection string format can
// also be used to connect to Azure SQL Server databases. The port is defined
// directly after the server name, so we'll create that and then create the
// final DSN string to pass to PDO.
$port = (isset($port)) ? ','.$port : '';
$dsn = "sqlsrv:Server={$host}{$port};Database={$database}";
return new PDO($dsn, $username, $password, $this->options($config));
}
}

View File

@@ -1,212 +0,0 @@
<?php namespace Laravel\Database\Eloquent;
class Hydrator {
/**
* Load the array of hydrated models and their eager relationships.
*
* @param Model $eloquent
* @return array
*/
public static function hydrate($eloquent)
{
$results = static::base(get_class($eloquent), $eloquent->query->get());
if (count($results) > 0)
{
foreach ($eloquent->includes as $include)
{
if ( ! method_exists($eloquent, $include))
{
throw new \LogicException("Attempting to eager load [$include], but the relationship is not defined.");
}
static::eagerly($eloquent, $results, $include);
}
}
return $results;
}
/**
* Hydrate the base models for a query.
*
* The resulting model array is keyed by the primary keys of the models.
* This allows the models to easily be matched to their children.
*
* @param string $class
* @param array $results
* @return array
*/
private static function base($class, $results)
{
$models = array();
foreach ($results as $result)
{
$model = new $class;
$model->attributes = (array) $result;
$model->exists = true;
if (isset($model->attributes['id']))
{
$models[$model->id] = $model;
}
else
{
$models[] = $model;
}
}
return $models;
}
/**
* Eagerly load a relationship.
*
* @param object $eloquent
* @param array $parents
* @param string $include
* @return void
*/
private static function eagerly($eloquent, &$parents, $include)
{
// We temporarily spoof the query attributes to allow the query to be fetched without
// any problems, since the belongs_to method actually gets the related attribute.
$first = reset($parents);
$eloquent->attributes = $first->attributes;
$relationship = $eloquent->$include();
$eloquent->attributes = array();
// Reset the WHERE clause and bindings on the query. We'll add our own WHERE clause soon.
// This will allow us to load a range of related models instead of only one.
$relationship->query->reset_where();
// Initialize the relationship attribute on the parents. As expected, "many" relationships
// are initialized to an array and "one" relationships are initialized to null.
foreach ($parents as &$parent)
{
$parent->ignore[$include] = (in_array($eloquent->relating, array('has_many', 'has_and_belongs_to_many'))) ? array() : null;
}
if (in_array($relating = $eloquent->relating, array('has_one', 'has_many', 'belongs_to')))
{
return static::$relating($relationship, $parents, $eloquent->relating_key, $include);
}
else
{
static::has_and_belongs_to_many($relationship, $parents, $eloquent->relating_key, $eloquent->relating_table, $include);
}
}
/**
* Eagerly load a 1:1 relationship.
*
* @param object $relationship
* @param array $parents
* @param string $relating_key
* @param string $relating
* @param string $include
* @return void
*/
private static function has_one($relationship, &$parents, $relating_key, $include)
{
foreach ($relationship->where_in($relating_key, array_keys($parents))->get() as $key => $child)
{
$parents[$child->$relating_key]->ignore[$include] = $child;
}
}
/**
* Eagerly load a 1:* relationship.
*
* @param object $relationship
* @param array $parents
* @param string $relating_key
* @param string $relating
* @param string $include
* @return void
*/
private static function has_many($relationship, &$parents, $relating_key, $include)
{
foreach ($relationship->where_in($relating_key, array_keys($parents))->get() as $key => $child)
{
$parents[$child->$relating_key]->ignore[$include][$child->id] = $child;
}
}
/**
* Eagerly load a 1:1 belonging relationship.
*
* @param object $relationship
* @param array $parents
* @param string $relating_key
* @param string $include
* @return void
*/
private static function belongs_to($relationship, &$parents, $relating_key, $include)
{
$keys = array();
foreach ($parents as &$parent)
{
$keys[] = $parent->$relating_key;
}
$children = $relationship->where_in('id', array_unique($keys))->get();
foreach ($parents as &$parent)
{
if (array_key_exists($parent->$relating_key, $children))
{
$parent->ignore[$include] = $children[$parent->$relating_key];
}
}
}
/**
* Eagerly load a many-to-many relationship.
*
* @param object $relationship
* @param array $parents
* @param string $relating_key
* @param string $relating_table
* @param string $include
*
* @return void
*/
private static function has_and_belongs_to_many($relationship, &$parents, $relating_key, $relating_table, $include)
{
// The model "has and belongs to many" method sets the SELECT clause; however, we need
// to clear it here since we will be adding the foreign key to the select.
$relationship->query->select = null;
$relationship->query->where_in($relating_table.'.'.$relating_key, array_keys($parents));
// The foreign key is added to the select to allow us to easily match the models back to their parents.
// Otherwise, there would be no apparent connection between the models to allow us to match them.
$children = $relationship->query->get(array(Model::table(get_class($relationship)).'.*', $relating_table.'.'.$relating_key));
$class = get_class($relationship);
foreach ($children as $child)
{
$related = new $class;
$related->attributes = (array) $child;
$related->exists = true;
// Remove the foreign key since it was only added to the query to help match the models.
unset($related->attributes[$relating_key]);
$parents[$child->$relating_key]->ignore[$include][$child->id] = $related;
}
}
}

View File

@@ -1,538 +0,0 @@
<?php namespace Laravel\Database\Eloquent;
use Laravel\Str;
use Laravel\Inflector;
use Laravel\Paginator;
use Laravel\Database\Manager as DB;
abstract class Model {
/**
* The connection that should be used for the model.
*
* @var string
*/
public static $connection;
/**
* Indicates if the model has creation and update timestamps.
*
* @var bool
*/
public static $timestamps = false;
/**
* The name of the auto-incrementing sequence associated with the model.
*
* @var string
*/
public static $sequence = null;
/**
* The model query instance.
*
* @var Query
*/
public $query;
/**
* Indicates if the model exists in the database.
*
* @var bool
*/
public $exists = false;
/**
* The model's attributes.
*
* Typically, a model has an attribute for each column on the table.
*
* @var array
*/
public $attributes = array();
/**
* The model's dirty attributes.
*
* @var array
*/
public $dirty = array();
/**
* The model's ignored attributes.
*
* Ignored attributes will not be saved to the database, and are
* primarily used to hold relationships.
*
* @var array
*/
public $ignore = array();
/**
* The relationships that should be eagerly loaded.
*
* @var array
*/
public $includes = array();
/**
* The relationship type the model is currently resolving.
*
* @var string
*/
public $relating;
/**
* The foreign key of the "relating" relationship.
*
* @var string
*/
public $relating_key;
/**
* The table name of the model being resolved.
*
* This is used during many-to-many eager loading.
*
* @var string
*/
public $relating_table;
/**
* Create a new Eloquent model instance.
*
* @param array $attributes
* @return void
*/
public function __construct($attributes = array())
{
$this->fill($attributes);
}
/**
* Set the attributes of the model using an array.
*
* @param array $attributes
* @return Model
*/
public function fill($attributes)
{
foreach ($attributes as $key => $value)
{
$this->$key = $value;
}
return $this;
}
/**
* Set the eagerly loaded models on the queryable model.
*
* @return Model
*/
private function _with()
{
$this->includes = func_get_args();
return $this;
}
/**
* Factory for creating queryable Eloquent model instances.
*
* @param string $class
* @return object
*/
public static function query($class)
{
$model = new $class;
// Since this method is only used for instantiating models for querying
// purposes, we will go ahead and set the Query instance on the model.
$model->query = DB::connection(static::$connection)->table(static::table($class));
return $model;
}
/**
* Get the table name for a model.
*
* @param string $class
* @return string
*/
public static function table($class)
{
if (property_exists($class, 'table')) return $class::$table;
return strtolower(Inflector::plural(static::model_name($class)));
}
/**
* Get an Eloquent model name without any namespaces.
*
* @param string|Model $model
* @return string
*/
public static function model_name($model)
{
$class = (is_object($model)) ? get_class($model) : $model;
$segments = array_reverse(explode('\\', $class));
return $segments[0];
}
/**
* Get all of the models from the database.
*
* @return array
*/
public static function all()
{
return Hydrator::hydrate(static::query(get_called_class()));
}
/**
* Get a model by the primary key.
*
* @param int $id
* @return mixed
*/
public static function find($id)
{
return static::query(get_called_class())->where('id', '=', $id)->first();
}
/**
* Get an array of models from the database.
*
* @return array
*/
private function _get()
{
return Hydrator::hydrate($this);
}
/**
* Get the first model result
*
* @return mixed
*/
private function _first()
{
return (count($results = $this->take(1)->_get()) > 0) ? reset($results) : null;
}
/**
* Get paginated model results as a Paginator instance.
*
* @param int $per_page
* @return Paginator
*/
private function _paginate($per_page = null)
{
$total = $this->query->count();
// The number of models to show per page may be specified as a static property
// on the model. The models shown per page may also be overriden for the model
// by passing the number into this method. If the models to show per page is
// not available via either of these avenues, a default number will be shown.
if (is_null($per_page))
{
$per_page = (property_exists(get_class($this), 'per_page')) ? static::$per_page : 20;
}
return Paginator::make($this->for_page(Paginator::page($total, $per_page), $per_page)->get(), $total, $per_page);
}
/**
* Retrieve the query for a 1:1 relationship.
*
* @param string $model
* @param string $foreign_key
* @return mixed
*/
public function has_one($model, $foreign_key = null)
{
$this->relating = __FUNCTION__;
return $this->has_one_or_many($model, $foreign_key);
}
/**
* Retrieve the query for a 1:* relationship.
*
* @param string $model
* @param string $foreign_key
* @return mixed
*/
public function has_many($model, $foreign_key = null)
{
$this->relating = __FUNCTION__;
return $this->has_one_or_many($model, $foreign_key);
}
/**
* Retrieve the query for a 1:1 or 1:* relationship.
*
* The default foreign key for has one and has many relationships is the name
* of the model with an appended _id. For example, the foreign key for a
* User model would be user_id. Photo would be photo_id, etc.
*
* @param string $model
* @param string $foreign_key
* @return mixed
*/
private function has_one_or_many($model, $foreign_key)
{
$this->relating_key = (is_null($foreign_key)) ? strtolower(static::model_name($this)).'_id' : $foreign_key;
return static::query($model)->where($this->relating_key, '=', $this->id);
}
/**
* Retrieve the query for a 1:1 belonging relationship.
*
* The default foreign key for belonging relationships is the name of the
* relationship method name with _id. So, if a model has a "manager" method
* returning a belongs_to relationship, the key would be manager_id.
*
* @param string $model
* @param string $foreign_key
* @return mixed
*/
public function belongs_to($model, $foreign_key = null)
{
$this->relating = __FUNCTION__;
if ( ! is_null($foreign_key))
{
$this->relating_key = $foreign_key;
}
else
{
list(, $caller) = debug_backtrace(false);
$this->relating_key = $caller['function'].'_id';
}
return static::query($model)->where('id', '=', $this->attributes[$this->relating_key]);
}
/**
* Retrieve the query for a *:* relationship.
*
* The default foreign key for many-to-many relations is the name of the model
* with an appended _id. This is the same convention as has_one and has_many.
*
* @param string $model
* @param string $table
* @param string $foreign_key
* @param string $associated_key
* @return mixed
*/
public function has_and_belongs_to_many($model, $table = null, $foreign_key = null, $associated_key = null)
{
$this->relating = __FUNCTION__;
$this->relating_table = (is_null($table)) ? $this->intermediate_table($model) : $table;
// Allowing the overriding of the foreign and associated keys provides
// the flexibility for self-referential many-to-many relationships.
$this->relating_key = (is_null($foreign_key)) ? strtolower(static::model_name($this)).'_id' : $foreign_key;
// The associated key is the foreign key name of the related model.
// If the related model is "Role", the key would be "role_id".
$associated_key = (is_null($associated_key)) ? strtolower(static::model_name($model)).'_id' : $associated_key;
return static::query($model)
->select(array(static::table($model).'.*'))
->join($this->relating_table, static::table($model).'.id', '=', $this->relating_table.'.'.$associated_key)
->where($this->relating_table.'.'.$this->relating_key, '=', $this->id);
}
/**
* Determine the intermediate table name for a given model.
*
* By default, the intermediate table name is the plural names of the models
* arranged alphabetically and concatenated with an underscore.
*
* @param string $model
* @return string
*/
private function intermediate_table($model)
{
$models = array(Inflector::plural(static::model_name($model)), Inflector::plural(static::model_name($this)));
sort($models);
return strtolower($models[0].'_'.$models[1]);
}
/**
* Save the model to the database.
*
* @return bool
*/
public function save()
{
// If the model does not have any dirty attributes, there is no reason
// to save it to the database.
if ($this->exists and count($this->dirty) == 0) return true;
$model = get_class($this);
// Since the model was instantiated using "new", a query instance has not been set.
// Only models being used for querying have their query instances set by default.
$this->query = DB::connection(static::$connection)->table(static::table($model));
if (property_exists($model, 'timestamps') and $model::$timestamps)
{
$this->timestamp();
}
// If the model already exists in the database, we will just update it.
// Otherwise, we will insert the model and set the ID attribute.
if ($this->exists)
{
$success = ($this->query->where_id($this->attributes['id'])->update($this->dirty) === 1);
}
else
{
$success = is_numeric($this->attributes['id'] = $this->query->insert_get_id($this->attributes, static::$sequence));
}
($this->exists = true) and $this->dirty = array();
return $success;
}
/**
* Set the creation and update timestamps on the model.
*
* @return void
*/
protected function timestamp()
{
$this->updated_at = date('Y-m-d H:i:s');
if ( ! $this->exists) $this->created_at = $this->updated_at;
}
/**
* Delete a model from the database.
*
* @param int $id
* @return int
*/
public function delete($id = null)
{
// If the delete method is being called on an existing model, we only want to delete
// that model. If it is being called from an Eloquent query model, it is probably
// the developer's intention to delete more than one model, so we will pass the
// delete statement to the query instance.
if ( ! $this->exists) return $this->query->delete();
$table = static::table(get_class($this));
return DB::connection(static::$connection)->table($table)->delete($this->id);
}
/**
* Magic method for retrieving model attributes.
*/
public function __get($key)
{
if (array_key_exists($key, $this->attributes))
{
return $this->attributes[$key];
}
// Is the requested item a model relationship that has already been loaded?
// All of the loaded relationships are stored in the "ignore" array.
elseif (array_key_exists($key, $this->ignore))
{
return $this->ignore[$key];
}
// Is the requested item a model relationship? If it is, we will dynamically
// load it and return the results of the relationship query.
elseif (method_exists($this, $key))
{
$query = $this->$key();
return $this->ignore[$key] = (in_array($this->relating, array('has_one', 'belongs_to'))) ? $query->first() : $query->get();
}
}
/**
* Magic Method for setting model attributes.
*/
public function __set($key, $value)
{
// If the key is a relationship, add it to the ignored attributes.
// Ignored attributes are not stored in the database.
if (method_exists($this, $key))
{
$this->ignore[$key] = $value;
}
else
{
$this->attributes[$key] = $value;
$this->dirty[$key] = $value;
}
}
/**
* Magic Method for determining if a model attribute is set.
*/
public function __isset($key)
{
return (array_key_exists($key, $this->attributes) or array_key_exists($key, $this->ignore));
}
/**
* Magic Method for unsetting model attributes.
*/
public function __unset($key)
{
unset($this->attributes[$key], $this->ignore[$key], $this->dirty[$key]);
}
/**
* Magic Method for handling dynamic method calls.
*/
public function __call($method, $parameters)
{
// To allow the "with", "get", "first", and "paginate" methods to be called both
// staticly and on an instance, we need to have private, underscored versions
// of the methods and handle them dynamically.
if (in_array($method, array('with', 'get', 'first', 'paginate')))
{
return call_user_func_array(array($this, '_'.$method), $parameters);
}
// All of the aggregate and persistance functions can be passed directly to the query
// instance. For these functions, we can simply return the response of the query.
if (in_array($method, array('insert', 'update', 'increment', 'decrement', 'abs', 'count', 'sum', 'min', 'max', 'avg')))
{
return call_user_func_array(array($this->query, $method), $parameters);
}
// Pass the method to the query instance. This allows the chaining of methods
// from the query builder, providing the same convenient query API as the
// query builder itself.
call_user_func_array(array($this->query, $method), $parameters);
return $this;
}
/**
* Magic Method for handling dynamic static method calls.
*/
public static function __callStatic($method, $parameters)
{
// Just pass the method to a model instance and let the __call method take care of it.
return call_user_func_array(array(static::query(get_called_class()), $method), $parameters);
}
}

View File

@@ -0,0 +1,108 @@
<?php namespace Laravel\Database;
abstract class Grammar {
/**
* The keyword identifier for the database system.
*
* @var string
*/
protected $wrapper = '"%s"';
/**
* Wrap a value in keyword identifiers.
*
* @param string $value
* @return string
*/
public function wrap($value)
{
// If the value being wrapped contains a column alias, we need to
// wrap it a little differently as each segment must be wrapped
// and not the entire string. We'll split the value on the "as"
// joiner to extract the column and the alias.
if (strpos(strtolower($value), ' as ') !== false)
{
$segments = explode(' ', $value);
return $this->wrap($segments[0]).' AS '.$this->wrap($segments[2]);
}
// Expressions should be injected into the query as raw strings,
// so we do not want to wrap them in any way. We'll just return
// the string value from the expression to be included.
if ($value instanceof Expression) return $value->get();
// Since columns may be prefixed with their corresponding table
// name so as to not make them ambiguous, we will need to wrap
// the table and the column in keyword identifiers.
foreach (explode('.', $value) as $segment)
{
if ($segment == '*')
{
$wrapped[] = $segment;
}
else
{
$wrapped[] = sprintf($this->wrapper, $segment);
}
}
return implode('.', $wrapped);
}
/**
* Create query parameters from an array of values.
*
* <code>
* Returns "?, ?, ?", which may be used as PDO place-holders
* $parameters = $grammar->parameterize(array(1, 2, 3));
*
* // Returns "?, "Taylor"" since an expression is used
* $parameters = $grammar->parameterize(array(1, DB::raw('Taylor')));
* </code>
*
* @param array $values
* @return string
*/
final public function parameterize($values)
{
return implode(', ', array_map(array($this, 'parameter'), $values));
}
/**
* Get the appropriate query parameter string for a value.
*
* <code>
* // Returns a "?" PDO place-holder
* $value = $grammar->parameter('Taylor Otwell');
*
* // Returns "Taylor Otwell" as the raw value of the expression
* $value = $grammar->parameter(DB::raw('Taylor Otwell'));
* </code>
*
* @param mixed $value
* @return string
*/
final public function parameter($value)
{
return ($value instanceof Expression) ? $value->get() : '?';
}
/**
* Create a comma-delimited list of wrapped column names.
*
* <code>
* // Returns ""Taylor", "Otwell"" when the identifier is quotes
* $columns = $grammar->columnize(array('Taylor', 'Otwell'));
* </code>
*
* @param array $columns
* @return string
*/
final public function columnize($columns)
{
return implode(', ', array_map(array($this, 'wrap'), $columns));
}
}

View File

@@ -1,134 +0,0 @@
<?php namespace Laravel\Database;
use Laravel\IoC;
use Laravel\Config;
class Manager {
/**
* The established database connections.
*
* @var array
*/
protected static $connections = array();
/**
* Get a database connection.
*
* If no database name is specified, the default connection will be returned.
*
* <code>
* // Get the default database connection for the application
* $connection = DB::connection();
*
* // Get a specific connection by passing the connection name
* $connection = DB::connection('mysql');
* </code>
*
* @param string $connection
* @return Connection
*/
public static function connection($connection = null)
{
if (is_null($connection)) $connection = Config::get('database.default');
if ( ! isset(static::$connections[$connection]))
{
$config = Config::get("database.connections.{$connection}");
if (is_null($config))
{
throw new \OutOfBoundsException("Connection is not defined for [$connection].");
}
static::$connections[$connection] = new Connection(static::connect($config), $config);
}
return static::$connections[$connection];
}
/**
* Get a PDO database connection for a given database configuration.
*
* @param array $config
* @return PDO
*/
protected static function connect($config)
{
// We allow the developer to place a "connector" option in the database
// configuration, which should have a Closure value. If the connector
// is present, we will use the Closure to retrieve the PDO connection
// to the database. This allows the flexiblity to connect to database
// systems that are not officially supported by the the framework.
if (isset($config['connector']))
{
return call_user_func($config['connector'], $config);
}
return static::connector($config['driver'])->connect($config);
}
/**
* Create a new database connector instance.
*
* The database connectors are responsible for simply establishing a PDO
* database connection given a configuration. This allows us to easily
* drop in support for new database systems by writing a connector.
*
* @param string $driver
* @return Connector
*/
protected static function connector($driver)
{
switch ($driver)
{
case 'sqlite':
return new Connectors\SQLite(DATABASE_PATH);
case 'mysql':
return new Connectors\MySQL;
case 'pgsql':
return new Connectors\Postgres;
default:
throw new \DomainException("Database driver [$driver] is not supported.");
}
}
/**
* Begin a fluent query against a table.
*
* @param string $table
* @param string $connection
* @return Queries\Query
*/
public static function table($table, $connection = null)
{
return static::connection($connection)->table($table);
}
/**
* Create a new database expression instance.
*
* Database expressions are used to inject raw SQL into a fluent query.
*
* @param string $value
* @return Expression
*/
public static function raw($value)
{
return new Expression($value);
}
/**
* Magic Method for calling methods on the default database connection.
*
* This provides a convenient API for querying or examining the default database connection.
*/
public static function __callStatic($method, $parameters)
{
return call_user_func_array(array(static::connection(), $method), $parameters);
}
}

View File

@@ -1,4 +1,10 @@
<?php namespace Laravel\Database; use Laravel\Paginator;
<?php namespace Laravel\Database;
use Closure;
use Laravel\Database;
use Laravel\Paginator;
use Laravel\Database\Query\Grammars\Grammar;
use Laravel\Database\Query\Grammars\SQLServer;
class Query {
@@ -12,7 +18,7 @@ class Query {
/**
* The query grammar instance.
*
* @var Grammars\Grammar
* @var Query\Grammars\Grammar
*/
public $grammar;
@@ -58,6 +64,13 @@ class Query {
*/
public $wheres;
/**
* The GROUP BY clauses.
*
* @var array
*/
public $groupings;
/**
* The ORDER BY clauses.
*
@@ -89,12 +102,12 @@ class Query {
/**
* Create a new query instance.
*
* @param Connection $connection
* @param Grammars\Grammar $grammar
* @param string $table
* @param Connection $connection
* @param Grammar $grammar
* @param string $table
* @return void
*/
public function __construct(Connection $connection, Grammars\Grammar $grammar, $table)
public function __construct(Connection $connection, Grammar $grammar, $table)
{
$this->from = $table;
$this->grammar = $grammar;
@@ -156,7 +169,7 @@ class Query {
}
/**
* Reset the where clause to its initial state. All bindings will be cleared.
* Reset the where clause to its initial state.
*
* @return void
*/
@@ -203,8 +216,16 @@ class Query {
* @param string $connector
* @return Query
*/
public function where($column, $operator, $value, $connector = 'AND')
public function where($column, $operator = null, $value = null, $connector = 'AND')
{
// If a CLosure is passed into the method, it means a nested where
// clause is being initiated, so we will take a different course
// of action than when the statement is just a simple where.
if ($column instanceof Closure)
{
return $this->where_nested($column, $connector);
}
$type = 'where';
$this->wheres[] = compact('type', 'column', 'operator', 'value', 'connector');
@@ -222,7 +243,7 @@ class Query {
* @param mixed $value
* @return Query
*/
public function or_where($column, $operator, $value)
public function or_where($column, $operator = null, $value = null)
{
return $this->where($column, $operator, $value, 'OR');
}
@@ -347,10 +368,35 @@ class Query {
}
/**
* Add dynamic where conditions to the query.
* Add a nested where condition to the query.
*
* Dynamic queries are caught by the __call magic method and are parsed here.
* They provide a convenient, expressive API for building simple conditions.
* @param Closure $callback
* @param string $connector
* @return Query
*/
protected function where_nested($callback, $connector)
{
$type = 'where_nested';
// To handle a nested where statement, we will actually instantiate a
// new Query instance and run the callback over that instance, which
// will allow the developer to have a fresh query to work with.
$query = new Query($this->connection, $this->grammar, $this->from);
// Once the callback has been run on the query, we will store the
// nested query instance on the where clause array so that it's
// passed to the query grammar.
call_user_func($callback, $query);
$this->wheres[] = compact('type', 'query', 'connector');
$this->bindings = array_merge($this->bindings, $query->bindings);
return $this;
}
/**
* Add dynamic where conditions to the query.
*
* @param string $method
* @param array $parameters
@@ -358,11 +404,11 @@ class Query {
*/
private function dynamic_where($method, $parameters)
{
// Strip the "where_" off of the method.
$finder = substr($method, 6);
// Split the column names from the connectors.
$segments = preg_split('/(_and_|_or_)/i', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);
$flags = PREG_SPLIT_DELIM_CAPTURE;
$segments = preg_split('/(_and_|_or_)/i', $finder, -1, $flags);
// The connector variable will determine which connector will be
// used for the condition. We'll change it as we come across new
@@ -377,6 +423,13 @@ class Query {
foreach ($segments as $segment)
{
// If the segment is not a boolean connector, we can assume it
// it is a column name, and we'll add it to the query as a new
// where clause.
//
// Otherwise, we'll store the connector so that we know how to
// connection the next where clause we find to the query, as
// all connectors should precede a new where clause.
if ($segment != '_and_' and $segment != '_or_')
{
$this->where($segment, '=', $parameters[$index], $connector);
@@ -392,6 +445,18 @@ class Query {
return $this;
}
/**
* Add a grouping to the query.
*
* @param string $column
* @return Query
*/
public function group_by($column)
{
$this->groupings[] = $column;
return $this;
}
/**
* Add an ordering to the query.
*
@@ -430,7 +495,7 @@ class Query {
}
/**
* Set the query limit and offset for a given page and item per page count.
* Set the query limit and offset for a given page.
*
* @param int $page
* @param int $per_page
@@ -461,16 +526,14 @@ class Query {
*/
public function only($column)
{
$this->select(array($column));
$sql = $this->grammar->select($this->select(array($column)));
return $this->connection->only($this->grammar->select($this), $this->bindings);
return $this->connection->only($sql, $this->bindings);
}
/**
* Execute the query as a SELECT statement and return the first result.
*
* If a single column is selected from the database, only the value of that column will be returned.
*
* @param array $columns
* @return mixed
*/
@@ -478,7 +541,51 @@ class Query {
{
$columns = (array) $columns;
return (count($results = $this->take(1)->get($columns)) > 0) ? $results[0] : null;
// Since we only need the first result, we'll go ahead and set the
// limit clause to 1, since this will be much faster than getting
// all of the rows and then only returning the first.
$results = $this->take(1)->get($columns);
return (count($results) > 0) ? $results[0] : null;
}
/**
* Get an array with the values of a given column.
*
* @param string $column
* @param string $key
* @return array
*/
public function lists($column, $key = null)
{
$columns = (is_null($key)) ? array($column) : array($column, $key);
$results = $this->get($columns);
// First we will get the array of values for the requested column.
// Of course, this array will simply have numeric keys. After we
// have this array we will determine if we need to key the array
// by another column from the result set.
$values = array_map(function($row) use ($column)
{
return $row->$column;
}, $results);
// If a key was provided, we will extract an array of keys and
// set the keys on the array of values using the array_combine
// function provided by PHP, which should give us the proper
// array form to return from the method.
if ( ! is_null($key))
{
return array_combine(array_map(function($row) use ($key)
{
return $row->$key;
}, $results), $values);
}
return $values;
}
/**
@@ -491,11 +598,25 @@ class Query {
{
if (is_null($this->selects)) $this->select($columns);
$results = $this->connection->query($this->grammar->select($this), $this->bindings);
$sql = $this->grammar->select($this);
$results = $this->connection->query($sql, $this->bindings);
// If the query has an offset and we are using the SQL Server grammar,
// we need to spin through the results and remove the "rownum" from
// each of the objects. Unfortunately SQL Server does not have an
// offset keyword, so we have to use row numbers in the query.
if ($this->offset > 0 and $this->grammar instanceof SQLServer)
{
array_walk($results, function($result)
{
unset($result->rownum);
});
}
// Reset the SELECT clause so more queries can be performed using
// the same instance. This is helpful for getting aggregates and
// then getting actual results.
// then getting actual results from the query.
$this->selects = null;
return $results;
@@ -512,11 +633,13 @@ class Query {
{
$this->aggregate = compact('aggregator', 'column');
$result = $this->connection->only($this->grammar->select($this), $this->bindings);
$sql = $this->grammar->select($this);
$result = $this->connection->only($sql, $this->bindings);
// Reset the aggregate so more queries can be performed using
// the same instance. This is helpful for getting aggregates
// and then getting actual results.
// and then getting actual results from the query.
$this->aggregate = null;
return $result;
@@ -531,17 +654,23 @@ class Query {
*/
public function paginate($per_page = 20, $columns = array('*'))
{
// Because some database engines may throw errors if we leave
// orderings on the query when retrieving the total number
// of records, we will remove all of the ordreings and put
// them back on the query after we have the count.
// Because some database engines may throw errors if we leave orderings
// on the query when retrieving the total number of records, we will
// remove all of the ordreings and put them back on the query after
// we have the count.
list($orderings, $this->orderings) = array($this->orderings, null);
$page = Paginator::page($total = $this->count(), $per_page);
$this->orderings = $orderings;
return Paginator::make($this->for_page($page, $per_page)->get($columns), $total, $per_page);
// Now we're ready to get the actual pagination results from the
// database table. The "for_page" method provides a convenient
// way to set the limit and offset so we get the correct span
// of results from the table.
$results = $this->for_page($page, $per_page)->get($columns);
return Paginator::make($results, $total, $per_page);
}
/**
@@ -559,17 +688,21 @@ class Query {
$bindings = array();
// We need to merge the the insert values into the array of the query
// bindings so that they will be bound to the PDO statement when it
// is executed by the database connection.
foreach ($values as $value)
{
$bindings = array_merge($bindings, array_values($value));
}
return $this->connection->query($this->grammar->insert($this, $values), $bindings);
$sql = $this->grammar->insert($this, $values);
return $this->connection->statement($sql, $bindings);
}
/**
* Insert an array of values into the database table and
* return the value of the ID column.
* Insert an array of values into the database table and return the ID.
*
* @param array $values
* @param string $sequence
@@ -577,8 +710,13 @@ class Query {
*/
public function insert_get_id($values, $sequence = null)
{
$this->connection->query($this->grammar->insert($this, $values), array_values($values));
$sql = $this->grammar->insert($this, $values);
$this->connection->statement($sql, array_values($values));
// Some database systems (Postgres) require a sequence name to be
// given when retrieving the auto-incrementing ID, so we'll pass
// the given sequence into the method just in case.
return (int) $this->connection->pdo->lastInsertId($sequence);
}
@@ -616,7 +754,10 @@ class Query {
*/
protected function adjust($column, $amount, $operator)
{
$value = Manager::raw($this->grammar->wrap($column).$operator.$amount);
// To make the adjustment to the column, we'll wrap the expression
// in an Expression instance, which forces the adjustment to be
// injected into the query as a string instead of bound.
$value = Database::raw($this->grammar->wrap($column).$operator.$amount);
return $this->update(array($column => $value));
}
@@ -629,9 +770,15 @@ class Query {
*/
public function update($values)
{
// For update statements, we need to merge the bindings such that
// the update values occur before the where bindings in the array
// since the set statements will precede any of the where clauses
// in the SQL syntax that is generated.
$bindings = array_merge(array_values($values), $this->bindings);
return $this->connection->query($this->grammar->update($this, $values), $bindings);
$sql = $this->grammar->update($this, $values);
return $this->connection->update($sql, $bindings);
}
/**
@@ -644,16 +791,23 @@ class Query {
*/
public function delete($id = null)
{
if ( ! is_null($id)) $this->where('id', '=', $id);
// If an ID is given to the method, we'll set the where clause
// to match on the value of the ID. This allows the developer
// to quickly delete a row by its primary key value.
if ( ! is_null($id))
{
$this->where('id', '=', $id);
}
return $this->connection->query($this->grammar->delete($this), $this->bindings);
$sql = $this->grammar->delete($this);
return $this->connection->delete($sql, $this->bindings);
}
/**
* Magic Method for handling dynamic functions.
*
* This method handles all calls to aggregate functions as well
* as the construction of dynamic where clauses.
* This method handles calls to aggregates as well as dynamic where clauses.
*/
public function __call($method, $parameters)
{
@@ -662,7 +816,7 @@ class Query {
return $this->dynamic_where($method, $parameters, $this);
}
if (in_array($method, array('abs', 'count', 'min', 'max', 'avg', 'sum')))
if (in_array($method, array('count', 'min', 'max', 'avg', 'sum')))
{
if ($method == 'count')
{
@@ -674,7 +828,7 @@ class Query {
}
}
throw new \BadMethodCallException("Method [$method] is not defined on the Query class.");
throw new \Exception("Method [$method] is not defined on the Query class.");
}
}

View File

@@ -1,61 +1,70 @@
<?php namespace Laravel\Database\Grammars;
<?php namespace Laravel\Database\Query\Grammars;
use Laravel\Arr;
use Laravel\Database\Query;
use Laravel\Database\Expression;
class Grammar {
/**
* The keyword identifier for the database system.
*
* @var string
*/
protected $wrapper = '"';
class Grammar extends \Laravel\Database\Grammar {
/**
* All of the query componenets in the order they should be built.
*
* Each derived compiler may adjust these components and place them in the
* order needed for its particular database system, providing greater
* control over how the query is structured.
*
* @var array
*/
protected $components = array(
'aggregate',
'selects',
'from',
'joins',
'wheres',
'orderings',
'limit',
'offset'
'aggregate', 'selects', 'from', 'joins', 'wheres',
'groupings', 'orderings', 'limit', 'offset',
);
/**
* Compile a SQL SELECT statement from a Query instance.
*
* The query will be compiled according to the order of the elements specified
* in the "components" property. The entire query is passed into each component
* compiler for convenience.
*
* @param Query $query
* @return string
*/
final public function select(Query $query)
public function select(Query $query)
{
$sql = array();
return $this->concatenate($this->components($query));
}
/**
* Generate the SQL for every component of the query.
*
* @param Query $query
* @return array
*/
final protected function components($query)
{
// Each portion of the statement is compiled by a function corresponding
// to an item in the components array. This lets us to keep the creation
// of the query very granular, and allows for the flexible customization
// of the query building process by each database system's grammar.
//
// Note that each component also corresponds to a public property on the
// query instance, allowing us to pass the appropriate data into each of
// the compiler functions.
foreach ($this->components as $component)
{
if ( ! is_null($query->$component))
{
$sql[] = call_user_func(array($this, $component), $query);
$sql[$component] = call_user_func(array($this, $component), $query);
}
}
return implode(' ', Arr::without($sql, array(null, '')));
return (array) $sql;
}
/**
* Concatenate an array of SQL segments, removing those that are empty.
*
* @param array $components
* @return string
*/
final protected function concatenate($components)
{
return implode(' ', array_filter($components, function($value)
{
return (string) $value !== '';
}));
}
/**
@@ -74,11 +83,6 @@ class Grammar {
/**
* Compile an aggregating SELECT clause for a query.
*
* If an aggregate function is called on the query instance, no select
* columns will be set, so it is safe to assume that the "selects"
* compiler function will not be called. We can simply build the
* aggregating select clause within this function.
*
* @param Query $query
* @return string
*/
@@ -92,9 +96,6 @@ class Grammar {
/**
* Compile the FROM clause for a query.
*
* This method should not handle the construction of "join" clauses.
* The join clauses will be constructured by their own compiler.
*
* @param Query $query
* @return string
*/
@@ -111,8 +112,13 @@ class Grammar {
*/
protected function joins(Query $query)
{
$format = '%s JOIN %s ON %s %s %s';
// We need to iterate through each JOIN clause that is attached to the
// query an translate it into SQL. The table and the columns will be
// wrapped in identifiers to avoid naming collisions.
//
// Once all of the JOINs have been compiled, we can concatenate them
// together using a single space, which should give us the complete
// set of joins in valid SQL that can appended to the query.
foreach ($query->joins as $join)
{
$table = $this->wrap($join['table']);
@@ -121,7 +127,7 @@ class Grammar {
$column2 = $this->wrap($join['column2']);
$sql[] = sprintf($format, $join['type'], $table, $column1, $join['operator'], $column2);
$sql[] = "{$join['type']} JOIN {$table} ON {$column1} {$join['operator']} {$column2}";
}
return implode(' ', $sql);
@@ -136,26 +142,44 @@ class Grammar {
final protected function wheres(Query $query)
{
// Each WHERE clause array has a "type" that is assigned by the query
// builder, and each type has its own compiler function. We will simply
// iterate through the where clauses and call the appropriate compiler
// for each clause.
// builder, and each type has its own compiler function. We will call
// the appropriate compiler for each where clause in the query.
//
// Keeping each particular where clause in its own "compiler" allows
// us to keep the query generation process very granular, making it
// easier to customize derived grammars for other databases.
foreach ($query->wheres as $where)
{
$sql[] = $where['connector'].' '.$this->{$where['type']}($where);
}
if (isset($sql)) return implode(' ', array_merge(array('WHERE 1 = 1'), $sql));
if (isset($sql))
{
// We attach the boolean connector to every where segment just
// for convenience. Once we have built the entire clause we'll
// remove the first instance of a connector from the clause.
return 'WHERE '.preg_replace('/AND |OR /', '', implode(' ', $sql), 1);
}
}
/**
* Compile a nested WHERE clause.
*
* @param array $where
* @return string
*/
protected function where_nested($where)
{
// To generate a nested WHERE clause, we'll just feed the query
// back into the "wheres" method. Once we have the clause, we
// will strip off the first six characters to get rid of the
// leading WHERE keyword.
return '('.substr($this->wheres($where['query']), 6).')';
}
/**
* Compile a simple WHERE clause.
*
* This method handles the compilation of the structures created by the
* "where" and "or_where" methods on the query builder.
*
* This method also handles database expressions, so care must be taken
* to implement this functionality in any derived database grammars.
*
* @param array $where
* @return string
*/
@@ -220,11 +244,22 @@ class Grammar {
* @param array $where
* @return string
*/
protected function where_raw($where)
final protected function where_raw($where)
{
return $where['sql'];
}
/**
* Compile the GROUP BY clause for a query.
*
* @param Query $query
* @return string
*/
protected function groupings(Query $query)
{
return 'GROUP BY '.$this->columnize($query->groupings);
}
/**
* Compile the ORDER BY clause for a query.
*
@@ -235,7 +270,9 @@ class Grammar {
{
foreach ($query->orderings as $ordering)
{
$sql[] = $this->wrap($ordering['column']).' '.strtoupper($ordering['direction']);
$direction = strtoupper($ordering['direction']);
$sql[] = $this->wrap($ordering['column']).' '.$direction;
}
return 'ORDER BY '.implode(', ', $sql);
@@ -266,7 +303,7 @@ class Grammar {
/**
* Compile a SQL INSERT statment from a Query instance.
*
* Note: This method handles the compilation of single row inserts and batch inserts.
* This method handles the compilation of single row inserts and batch inserts.
*
* @param Query $query
* @param array $values
@@ -274,14 +311,16 @@ class Grammar {
*/
public function insert(Query $query, $values)
{
// Force every insert to be treated like a batch insert. This simple makes
$table = $this->wrap($query->from);
// Force every insert to be treated like a batch insert. This simply makes
// creating the SQL syntax a little easier on us since we can always treat
// the values as if it is an array containing multiple inserts.
if ( ! is_array(reset($values))) $values = array($values);
// Since we only care about the column names, we can pass any of the insert
// arrays into the "columnize" method. The names should be the same for
// every insert to the table.
// arrays into the "columnize" method. The columns should be the same for
// every insert to the table so we can just use the first record.
$columns = $this->columnize(array_keys(reset($values)));
// Build the list of parameter place-holders of values bound to the query.
@@ -289,24 +328,26 @@ class Grammar {
// just use the first array of values.
$parameters = $this->parameterize(reset($values));
$parameters = implode(', ', array_fill(0, count($values), '('.$parameters.')'));
$parameters = implode(', ', array_fill(0, count($values), "($parameters)"));
return 'INSERT INTO '.$this->wrap($query->from).' ('.$columns.') VALUES '.$parameters;
return "INSERT INTO {$table} ({$columns}) VALUES {$parameters}";
}
/**
* Compile a SQL UPDATE statment from a Query instance.
*
* Note: Since UPDATE statements can be limited by a WHERE clause,
* this method will use the same WHERE clause compilation
* functions as the "select" method.
*
* @param Query $query
* @param array $values
* @return string
*/
public function update(Query $query, $values)
{
$table = $this->wrap($query->from);
// Each column in the UPDATE statement needs to be wrapped in keyword
// identifiers, and a place-holder needs to be created for each value
// in the array of bindings. Of course, if the value of the binding
// is an expression, the expression string will be injected.
foreach ($values as $column => $value)
{
$columns[] = $this->wrap($column).' = '.$this->parameter($value);
@@ -314,7 +355,11 @@ class Grammar {
$columns = implode(', ', $columns);
return trim('UPDATE '.$this->wrap($query->from).' SET '.$columns.' '.$this->wheres($query));
// UPDATE statements may be constrained by a WHERE clause, so we'll
// run the entire where compilation process for those contraints.
// This is easily achieved by passing the query to the "wheres"
// method which will call all of the where compilers.
return trim("UPDATE {$table} SET {$columns} ".$this->wheres($query));
}
/**
@@ -325,103 +370,13 @@ class Grammar {
*/
public function delete(Query $query)
{
return trim('DELETE FROM '.$this->wrap($query->from).' '.$this->wheres($query));
}
$table = $this->wrap($query->from);
/**
* The following functions primarily serve as utility functions for
* the grammar. They perform tasks such as wrapping values in keyword
* identifiers or creating variable lists of bindings.
*/
/**
* Create a comma-delimited list of wrapped column names.
*
* @param array $columns
* @return string
*/
final public function columnize($columns)
{
return implode(', ', array_map(array($this, 'wrap'), $columns));
}
/**
* Wrap a value in keyword identifiers.
*
* They keyword identifier used by the method is specified as
* a property on the grammar class so it can be conveniently
* overriden without changing the wrapping logic itself.
*
* @param string $value
* @return string
*/
public function wrap($value)
{
// If the value being wrapped contains a column alias, we need to wrap
// it a little differently since each segment must be wrapped and not
// the entire string.
if (strpos(strtolower($value), ' as ') !== false)
{
return $this->alias($value);
}
// Expressions should be injected into the query as raw strings, so we
// do not want to wrap them in any way. We will just return the string
// value from the expression to be included in the query.
if ($value instanceof Expression) return $value->get();
foreach (explode('.', $value) as $segment)
{
if ($segment === '*')
{
$wrapped[] = $segment;
}
else
{
$wrapped[] = $this->wrapper.$segment.$this->wrapper;
}
}
return implode('.', $wrapped);
}
/**
* Wrap an alias in keyword identifiers.
*
* @param string $value
* @return string
*/
protected function alias($value)
{
$segments = explode(' ', $value);
return $this->wrap($segments[0]).' AS '.$this->wrap($segments[2]);
}
/**
* Create query parameters from an array of values.
*
* @param array $values
* @return string
*/
public function parameterize($values)
{
return implode(', ', array_map(array($this, 'parameter'), $values));
}
/**
* Get the appropriate query parameter string for a value.
*
* If the value is an expression, the raw expression string should
* be returned, otherwise, the parameter place-holder will be
* returned by the method.
*
* @param mixed $value
* @return string
*/
public function parameter($value)
{
return ($value instanceof Expression) ? $value->get() : '?';
// Like the UPDATE statement, the DELETE statement is constrained
// by WHERE clauses, so we'll need to run the "wheres" method to
// make the WHERE clauses for the query. The "wheres" method
// encapsulates the logic to create the full WHERE clause.
return trim("DELETE FROM {$table} ".$this->wheres($query));
}
}

View File

@@ -1,4 +1,4 @@
<?php namespace Laravel\Database\Grammars;
<?php namespace Laravel\Database\Query\Grammars;
class MySQL extends Grammar {
@@ -7,6 +7,6 @@ class MySQL extends Grammar {
*
* @var string
*/
protected $wrapper = '`';
protected $wrapper = '`%s`';
}

View File

@@ -0,0 +1,139 @@
<?php namespace Laravel\Database\Query\Grammars;
use Laravel\Database\Query;
class SQLServer extends Grammar {
/**
* The keyword identifier for the database system.
*
* @var string
*/
protected $wrapper = '[%s]';
/**
* Compile a SQL SELECT statement from a Query instance.
*
* @param Query $query
* @return string
*/
public function select(Query $query)
{
$sql = parent::components($query);
// SQL Server does not currently implement an "OFFSET" type keyword, so we
// actually have to generate the ANSI standard SQL for doing offset like
// functionality. In the next version of SQL Server, an OFFSET like
// keyword is included for convenience.
if ($query->offset > 0)
{
return $this->ansi_offset($query, $sql);
}
// Once all of the clauses have been compiled, we can join them all as
// one statement. Any segments that are null or an empty string will
// be removed from the array of clauses before they are imploded.
return $this->concatenate($sql);
}
/**
* Compile the SELECT clause for a query.
*
* @param Query $query
* @return string
*/
protected function selects(Query $query)
{
$select = ($query->distinct) ? 'SELECT DISTINCT ' : 'SELECT ';
// Instead of using a "LIMIT" keyword, SQL Server uses the "TOP"
// keyword within the SELECT statement. So, if we have a limit,
// we will add it here.
//
// We will not add the TOP clause if there is an offset however,
// since we will have to handle offsets using the ANSI syntax
// and will need to remove the TOP clause in that situation.
if ($query->limit > 0 and $query->offset <= 0)
{
$select .= 'TOP '.$query->limit.' ';
}
return $select.$this->columnize($query->selects);
}
/**
* Generate the ANSI standard SQL for an offset clause.
*
* @param Query $query
* @param array $components
* @return array
*/
protected function ansi_offset(Query $query, $components)
{
// An ORDER BY clause is required to make this offset query
// work, so if one doesn't exist, we'll just create a dummy
// clause to satisfy the database.
if ( ! isset($components['orderings']))
{
$components['orderings'] = 'ORDER BY (SELECT 0)';
}
// We need to add the row number to the query results so we
// can compare it against the offset and limit values given
// for the statement. To do that we'll add an expression to
// the select statement for the row number.
$orderings = $components['orderings'];
$components['selects'] .= ", ROW_NUMBER() OVER ({$orderings}) AS RowNum";
unset($components['orderings']);
$start = $query->offset + 1;
// Next we need to calculate the constraint that should be
// placed on the row number to get the correct offset and
// limit on the query. If a limit has not been set, we'll
// only add a constraint to handle offset.
if ($query->limit > 0)
{
$finish = $query->offset + $query->limit;
$constraint = "BETWEEN {$start} AND {$finish}";
}
else
{
$constraint = ">= {$start}";
}
// Now, we're finally ready to build the final SQL query.
// We'll create a common table expression with the query
// and then select all of the results from it where the
// row number is between oru given limit and offset.
$sql = $this->concatenate($components);
return "SELECT * FROM ($sql) AS TempTable WHERE RowNum {$constraint}";
}
/**
* Compile the LIMIT clause for a query.
*
* @param Query $query
* @return string
*/
protected function limit(Query $query)
{
return;
}
/**
* Compile the OFFSET clause for a query.
*
* @param Query $query
* @return string
*/
protected function offset(Query $query)
{
return;
}
}

119
laravel/database/schema.php Normal file
View File

@@ -0,0 +1,119 @@
<?php namespace Laravel\Database;
use Laravel\Fluent;
use Laravel\Database as DB;
class Schema {
/**
* Begin a fluent schema operation on a database table.
*
* @param string $table
* @param Closure $callback
* @return void
*/
public static function table($table, $callback)
{
call_user_func($callback, $table = new Schema\Table($table));
static::implications($table);
return static::execute($table);
}
/**
* Execute the given schema operation against the database.
*
* @param Schema\Table $table
* @return void
*/
public static function execute($table)
{
foreach ($table->commands as $command)
{
$connection = DB::connection($table->connection);
$grammar = static::grammar($connection->driver());
// Each grammar has a function that corresponds to the command type
// and is responsible for building that's commands SQL. This lets
// the SQL generation stay very granular and makes it simply to
// add new database systems to the schema system.
if (method_exists($grammar, $method = $command->type))
{
$statements = $grammar->$method($table, $command);
// Once we have the statements, we will cast them to an array even
// though not all of the commands return an array. This is just in
// case the command needs to run more than one query to do what
// it needs to do what is requested by the developer.
foreach ((array) $statements as $statement)
{
$connection->statement($statement);
}
}
}
}
/**
* Add any implicit commands to the schema table operation.
*
* @param Schema\Table $table
* @return void
*/
protected static function implications($table)
{
// If the developer has specified columns for the table and the
// table is not being created, we will assume they simply want
// to add the columns to the table, and will generate an add
// command for them, adding the columns to the command.
if (count($table->columns) > 0 and ! $table->creating())
{
$command = new Fluent(array('type' => 'add'));
array_unshift($table->commands, $command);
}
// For some extra syntax sugar, we'll check for any implicit
// indexes on the table. The developer may specify the index
// type on the fluent column declaration. Here we'll find
// any such implicit index and add the actual command.
foreach ($table->columns as $column)
{
foreach (array('primary', 'unique', 'fulltext', 'index') as $key)
{
if ($column->$key === true)
{
$table->$key($column->name);
}
}
}
}
/**
* Create the appropriate schema grammar for the driver.
*
* @param string $driver
* @return Grammar
*/
public static function grammar($driver)
{
switch ($driver)
{
case 'mysql':
return new Schema\Grammars\MySQL;
case 'pgsql':
return new Schema\Grammars\Postgres;
case 'sqlsrv':
return new Schema\Grammars\SQLServer;
case 'sqlite':
return new Schema\Grammars\SQLite;
}
throw new \Exception("Schema operations not supported for [$driver].");
}
}

View File

@@ -0,0 +1,38 @@
<?php namespace Laravel\Database\Schema\Grammars;
use Laravel\Fluent;
use Laravel\Database\Schema\Table;
abstract class Grammar extends \Laravel\Database\Grammar {
/**
* Get the appropriate data type definition for the column.
*
* @param Fluent $column
* @return string
*/
protected function type(Fluent $column)
{
return $this->{'type_'.$column->type}($column);
}
/**
* Wrap a value in keyword identifiers.
*
* @param Table|string $value
* @return string
*/
public function wrap($value)
{
// This method is primarily for convenience so we can just pass a
// column or table instance into the wrap method without sending
// in the name each time we need to wrap one of these objects.
if ($value instanceof Table or $value instanceof Fluent)
{
$value = $value->name;
}
return parent::wrap($value);
}
}

View File

@@ -0,0 +1,386 @@
<?php namespace Laravel\Database\Schema\Grammars;
use Laravel\Fluent;
use Laravel\Database\Schema\Table;
class MySQL extends Grammar {
/**
* The keyword identifier for the database system.
*
* @var string
*/
public $wrapper = '`%s`';
/**
* Generate the SQL statements for a table creation command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function create(Table $table, Fluent $command)
{
$columns = implode(', ', $this->columns($table));
// First we will generate the base table creation statement. Other than
// auto-incrementing keys, no indexes will be created during the first
// creation of the table. They will be added in separate commands.
$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')';
// MySQL supports various "engines" for database tables. If an engine
// was specified by the developer, we will set it after adding the
// columns the table creation statement.
if ( ! is_null($table->engine))
{
$sql .= ' ENGINE = '.$table->engine;
}
return $sql;
}
/**
* Geenrate the SQL statements for a table modification command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function add(Table $table, Fluent $command)
{
$columns = $this->columns($table);
// Once we the array of column definitions, we need to add "add"
// to the front of each definition, then we'll concatenate the
// definitions using commas like normal and generate the SQL.
$columns = implode(', ', array_map(function($column)
{
return 'ADD '.$column;
}, $columns));
return 'ALTER TABLE '.$this->wrap($table).' '.$columns;
}
/**
* Create the individual column definitions for the table.
*
* @param Table $table
* @return array
*/
protected function columns(Table $table)
{
$columns = array();
foreach ($table->columns as $column)
{
// Each of the data type's have their own definition creation method,
// which is responsible for creating the SQL for the type. This lets
// us to keep the syntax easy and fluent, while translating the
// types to the types used by the database.
$sql = $this->wrap($column).' '.$this->type($column);
$elements = array('nullable', 'defaults', 'incrementer');
foreach ($elements as $element)
{
$sql .= $this->$element($table, $column);
}
$columns[] = $sql;
}
return $columns;
}
/**
* Get the SQL syntax for indicating if a column is nullable.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function nullable(Table $table, Fluent $column)
{
return ($column->nullable) ? ' NULL' : ' NOT NULL';
}
/**
* Get the SQL syntax for specifying a default value on a column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function defaults(Table $table, Fluent $column)
{
if ( ! is_null($column->default))
{
return " DEFAULT '".$column->default."'";
}
}
/**
* Get the SQL syntax for defining an auto-incrementing column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function incrementer(Table $table, Fluent $column)
{
if ($column->type == 'integer' and $column->increment)
{
return ' AUTO_INCREMENT PRIMARY KEY';
}
}
/**
* Generate the SQL statement for creating a primary key.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function primary(Table $table, Fluent $command)
{
return $this->key($table, $command, 'PRIMARY KEY');
}
/**
* Generate the SQL statement for creating a unique index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function unique(Table $table, Fluent $command)
{
return $this->key($table, $command, 'UNIQUE');
}
/**
* Generate the SQL statement for creating a full-text index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function fulltext(Table $table, Fluent $command)
{
return $this->key($table, $command, 'FULLTEXT');
}
/**
* Generate the SQL statement for creating a regular index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function index(Table $table, Fluent $command)
{
return $this->key($table, $command, 'INDEX');
}
/**
* Generate the SQL statement for creating a new index.
*
* @param Table $table
* @param Fluent $command
* @param string $type
* @return string
*/
protected function key(Table $table, Fluent $command, $type)
{
$keys = $this->columnize($command->columns);
$name = $command->name;
return 'ALTER TABLE '.$this->wrap($table)." ADD {$type} {$name}({$keys})";
}
/**
* Generate the SQL statement for a drop table command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop(Table $table, Fluent $command)
{
return 'DROP TABLE '.$this->wrap($table);
}
/**
* Generate the SQL statement for a drop column command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_column(Table $table, Fluent $command)
{
$columns = array_map(array($this, 'wrap'), $command->columns);
// Once we the array of column names, we need to add "drop" to the
// front of each column, then we'll concatenate the columns using
// commas and generate the alter statement SQL.
$columns = implode(', ', array_map(function($column)
{
return 'DROP '.$column;
}, $columns));
return 'ALTER TABLE '.$this->wrap($table).' '.$columns;
}
/**
* Generate the SQL statement for a drop primary key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_primary(Table $table, Fluent $command)
{
return 'ALTER TABLE '.$this->wrap($table).' DROP PRIMARY KEY';
}
/**
* Generate the SQL statement for a drop unqique key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_unique(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop full-text key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_fulltext(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop unqique key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_index(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
protected function drop_key(Table $table, Fluent $command)
{
return 'ALTER TABLE '.$this->wrap($table)." DROP INDEX {$command->name}";
}
/**
* Generate the data-type definition for a string.
*
* @param Fluent $column
* @return string
*/
protected function type_string(Fluent $column)
{
return 'VARCHAR('.$column->length.')';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_integer(Fluent $column)
{
return 'INT';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_float(Fluent $column)
{
return 'FLOAT';
}
/**
* Generate the data-type definition for a boolean.
*
* @param Fluent $column
* @return string
*/
protected function type_boolean(Fluent $column)
{
return 'TINYINT';
}
/**
* Generate the data-type definition for a date.
*
* @param Fluent $column
* @return string
*/
protected function type_date(Fluent $column)
{
return 'DATETIME';
}
/**
* Generate the data-type definition for a timestamp.
*
* @param Fluent $column
* @return string
*/
protected function type_timestamp(Fluent $column)
{
return 'TIMESTAMP';
}
/**
* Generate the data-type definition for a text column.
*
* @param Fluent $column
* @return string
*/
protected function type_text(Fluent $column)
{
return 'TEXT';
}
/**
* Generate the data-type definition for a blob.
*
* @param Fluent $column
* @return string
*/
protected function type_blob(Fluent $column)
{
return 'BLOB';
}
}

View File

@@ -0,0 +1,381 @@
<?php namespace Laravel\Database\Schema\Grammars;
use Laravel\Fluent;
use Laravel\Database\Schema\Table;
class Postgres extends Grammar {
/**
* Generate the SQL statements for a table creation command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function create(Table $table, Fluent $command)
{
$columns = implode(', ', $this->columns($table));
// First we will generate the base table creation statement. Other than
// auto-incrementing keys, no indexes will be created during the first
// creation of the table. They will be added in separate commands.
$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')';
return $sql;
}
/**
* Geenrate the SQL statements for a table modification command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function add(Table $table, Fluent $command)
{
$columns = $this->columns($table);
// Once we the array of column definitions, we'll add "add column"
// to the front of each definition, then we'll concatenate the
// definitions using commas like normal and generate the SQL.
$columns = implode(', ', array_map(function($column)
{
return 'ADD COLUMN '.$column;
}, $columns));
return 'ALTER TABLE '.$this->wrap($table).' '.$columns;
}
/**
* Create the individual column definitions for the table.
*
* @param Table $table
* @return array
*/
protected function columns(Table $table)
{
$columns = array();
foreach ($table->columns as $column)
{
// Each of the data type's have their own definition creation method,
// which is responsible for creating the SQL for the type. This lets
// us to keep the syntax easy and fluent, while translating the
// types to the types used by the database.
$sql = $this->wrap($column).' '.$this->type($column);
$elements = array('incrementer', 'nullable', 'defaults');
foreach ($elements as $element)
{
$sql .= $this->$element($table, $column);
}
$columns[] = $sql;
}
return $columns;
}
/**
* Get the SQL syntax for indicating if a column is nullable.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function nullable(Table $table, Fluent $column)
{
return ($column->nullable) ? ' NULL' : ' NOT NULL';
}
/**
* Get the SQL syntax for specifying a default value on a column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function defaults(Table $table, Fluent $column)
{
if ( ! is_null($column->default))
{
return " DEFAULT '".$column->default."'";
}
}
/**
* Get the SQL syntax for defining an auto-incrementing column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function incrementer(Table $table, Fluent $column)
{
// We don't actually need to specify an "auto_increment" keyword since
// we handle the auto-increment definition in the type definition for
// integers by changing the type to "serial", which is a convenient
// notational short-cut provided by Postgres.
if ($column->type == 'integer' and $column->increment)
{
return ' PRIMARY KEY';
}
}
/**
* Generate the SQL statement for creating a primary key.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function primary(Table $table, Fluent $command)
{
$columns = $this->columnize($command->columns);
return 'ALTER TABLE '.$this->wrap($table)." ADD PRIMARY KEY ({$columns})";
}
/**
* Generate the SQL statement for creating a unique index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function unique(Table $table, Fluent $command)
{
return $this->key($table, $command, true);
}
/**
* Generate the SQL statement for creating a full-text index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function fulltext(Table $table, Fluent $command)
{
$name = $command->name;
$columns = $this->columnize($command->columns);
return "CREATE INDEX {$name} ON ".$this->wrap($table)." USING gin({$columns})";
}
/**
* Generate the SQL statement for creating a regular index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function index(Table $table, Fluent $command)
{
return $this->key($table, $command);
}
/**
* Generate the SQL statement for creating a new index.
*
* @param Table $table
* @param Fluent $command
* @param bool $unique
* @return string
*/
protected function key(Table $table, Fluent $command, $unique = false)
{
$columns = $this->columnize($command->columns);
$create = ($unique) ? 'CREATE UNIQUE' : 'CREATE';
return $create." INDEX {$command->name} ON ".$this->wrap($table)." ({$columns})";
}
/**
* Generate the SQL statement for a drop table command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop(Table $table, Fluent $command)
{
return 'DROP TABLE '.$this->wrap($table);
}
/**
* Generate the SQL statement for a drop column command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_column(Table $table, Fluent $command)
{
$columns = array_map(array($this, 'wrap'), $command->columns);
// Once we the array of column names, we need to add "drop" to the
// front of each column, then we'll concatenate the columns using
// commas and generate the alter statement SQL.
$columns = implode(', ', array_map(function($column)
{
return 'DROP COLUMN '.$column;
}, $columns));
return 'ALTER TABLE '.$this->wrap($table).' '.$columns;
}
/**
* Generate the SQL statement for a drop primary key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_primary(Table $table, Fluent $command)
{
return 'ALTER TABLE '.$this->wrap($table).' DROP CONSTRAINT '.$table->name.'_pkey';
}
/**
* Generate the SQL statement for a drop unqique key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_unique(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop full-text key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_fulltext(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop index command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_index(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
protected function drop_key(Table $table, Fluent $command)
{
return 'DROP INDEX '.$command->name;
}
/**
* Generate the data-type definition for a string.
*
* @param Fluent $column
* @return string
*/
protected function type_string(Fluent $column)
{
return 'VARCHAR('.$column->length.')';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_integer(Fluent $column)
{
return ($column->increment) ? 'SERIAL' : 'INTEGER';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_float(Fluent $column)
{
return 'REAL';
}
/**
* Generate the data-type definition for a boolean.
*
* @param Fluent $column
* @return string
*/
protected function type_boolean(Fluent $column)
{
return 'SMALLINT';
}
/**
* Generate the data-type definition for a date.
*
* @param Fluent $column
* @return string
*/
protected function type_date(Fluent $column)
{
return 'TIMESTAMP';
}
/**
* Generate the data-type definition for a timestamp.
*
* @param Fluent $column
* @return string
*/
protected function type_timestamp(Fluent $column)
{
return 'TIMESTAMP';
}
/**
* Generate the data-type definition for a text column.
*
* @param Fluent $column
* @return string
*/
protected function type_text(Fluent $column)
{
return 'TEXT';
}
/**
* Generate the data-type definition for a blob.
*
* @param Fluent $column
* @return string
*/
protected function type_blob(Fluent $column)
{
return 'BYTEA';
}
}

View File

@@ -0,0 +1,344 @@
<?php namespace Laravel\Database\Schema\Grammars;
use Laravel\Fluent;
use Laravel\Database\Schema\Table;
class SQLite extends Grammar {
/**
* Generate the SQL statements for a table creation command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function create(Table $table, Fluent $command)
{
$columns = implode(', ', $this->columns($table));
// First we will generate the base table creation statement. Other than
// auto-incrementing keys, no indexes will be created during the first
// creation of the table. They will be added in separate commands.
$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns;
// SQLite does not allow adding a primary key as a command apart from
// when the table is initially created, so we'll need to sniff out
// any primary keys here and add them to the table.
//
// Because of this, this class does not have the typical "primary"
// method as it would be pointless since the primary keys can't
// be set on anything but the table creation statement.
$primary = array_first($table->commands, function($key, $value)
{
return $value->type == 'primary';
});
// If we found primary key in the array of commands, we'll create
// the SQL for the key addition and append it to the SQL table
// creation statement for the schema table.
if ( ! is_null($primary))
{
$columns = $this->columnize($primary->columns);
$sql .= ", PRIMARY KEY ({$columns})";
}
return $sql .= ')';
}
/**
* Geenrate the SQL statements for a table modification command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function add(Table $table, Fluent $command)
{
$columns = $this->columns($table);
// Once we have an array of all of the column definitions, we need to
// spin through each one and prepend "ADD COLUMN" to each of them,
// which is the syntax used by SQLite when adding columns.
$columns = array_map(function($column)
{
return 'ADD COLUMN '.$column;
}, $columns);
// SQLite only allows one column to be added in an ALTER statement,
// so we will create an array of statements and return them all to
// the schema manager, which will execute each one.
foreach ($columns as $column)
{
$sql[] = 'ALTER TABLE '.$this->wrap($table).' '.$column;
}
return (array) $sql;
}
/**
* Create the individual column definitions for the table.
*
* @param Table $table
* @return array
*/
protected function columns(Table $table)
{
$columns = array();
foreach ($table->columns as $column)
{
// Each of the data type's have their own definition creation method
// which is responsible for creating the SQL for the type. This lets
// us to keep the syntax easy and fluent, while translating the
// types to the types used by the database.
$sql = $this->wrap($column).' '.$this->type($column);
$elements = array('nullable', 'defaults', 'incrementer');
foreach ($elements as $element)
{
$sql .= $this->$element($table, $column);
}
$columns[] = $sql;
}
return $columns;
}
/**
* Get the SQL syntax for indicating if a column is nullable.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function nullable(Table $table, Fluent $column)
{
return ($column->nullable) ? ' NULL' : ' NOT NULL';
}
/**
* Get the SQL syntax for specifying a default value on a column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function defaults(Table $table, Fluent $column)
{
if ( ! is_null($column->default))
{
return ' DEFAULT '.$this->wrap($column->default);
}
}
/**
* Get the SQL syntax for defining an auto-incrementing column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function incrementer(Table $table, Fluent $column)
{
if ($column->type == 'integer' and $column->increment)
{
return ' PRIMARY KEY AUTOINCREMENT';
}
}
/**
* Generate the SQL statement for creating a unique index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function unique(Table $table, Fluent $command)
{
return $this->key($table, $command, true);
}
/**
* Generate the SQL statement for creating a full-text index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function fulltext(Table $table, Fluent $command)
{
$columns = $this->columnize($command->columns);
return 'CREATE VIRTUAL TABLE '.$this->wrap($table)." USING fts4({$columns})";
}
/**
* Generate the SQL statement for creating a regular index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function index(Table $table, Fluent $command)
{
return $this->key($table, $command);
}
/**
* Generate the SQL statement for creating a new index.
*
* @param Table $table
* @param Fluent $command
* @param bool $unique
* @return string
*/
protected function key(Table $table, Fluent $command, $unique = false)
{
$columns = $this->columnize($command->columns);
$create = ($unique) ? 'CREATE UNIQUE' : 'CREATE';
return $create." INDEX {$command->name} ON ".$this->wrap($table)." ({$columns})";
}
/**
* Generate the SQL statement for a drop table command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop(Table $table, Fluent $command)
{
return 'DROP TABLE '.$this->wrap($table);
}
/**
* Generate the SQL statement for a drop unqique key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_unique(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop unqique key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_index(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
protected function drop_key(Table $table, Fluent $command)
{
return 'DROP INDEX '.$this->wrap($command->name);
}
/**
* Generate the data-type definition for a string.
*
* @param Fluent $column
* @return string
*/
protected function type_string(Fluent $column)
{
return 'VARCHAR';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_integer(Fluent $column)
{
return 'INTEGER';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_float(Fluent $column)
{
return 'FLOAT';
}
/**
* Generate the data-type definition for a boolean.
*
* @param Fluent $column
* @return string
*/
protected function type_boolean(Fluent $column)
{
return 'INTEGER';
}
/**
* Generate the data-type definition for a date.
*
* @param Fluent $column
* @return string
*/
protected function type_date(Fluent $column)
{
return 'DATETIME';
}
/**
* Generate the data-type definition for a timestamp.
*
* @param Fluent $column
* @return string
*/
protected function type_timestamp(Fluent $column)
{
return 'DATETIME';
}
/**
* Generate the data-type definition for a text column.
*
* @param Fluent $column
* @return string
*/
protected function type_text(Fluent $column)
{
return 'TEXT';
}
/**
* Generate the data-type definition for a blob.
*
* @param Fluent $column
* @return string
*/
protected function type_blob(Fluent $column)
{
return 'BLOB';
}
}

View File

@@ -0,0 +1,402 @@
<?php namespace Laravel\Database\Schema\Grammars;
use Laravel\Fluent;
use Laravel\Database\Schema\Table;
class SQLServer extends Grammar {
/**
* The keyword identifier for the database system.
*
* @var string
*/
public $wrapper = '[%s]';
/**
* Generate the SQL statements for a table creation command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function create(Table $table, Fluent $command)
{
$columns = implode(', ', $this->columns($table));
// First we will generate the base table creation statement. Other than
// auto-incrementing keys, no indexes will be created during the first
// creation of the table. They will be added in separate commands.
$sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')';
return $sql;
}
/**
* Geenrate the SQL statements for a table modification command.
*
* @param Table $table
* @param Fluent $command
* @return array
*/
public function add(Table $table, Fluent $command)
{
$columns = $this->columns($table);
// Once we the array of column definitions, we need to add "add"
// to the front of each definition, then we'll concatenate the
// definitions using commas like normal and generate the SQL.
$columns = implode(', ', array_map(function($column)
{
return 'ADD '.$column;
}, $columns));
return 'ALTER TABLE '.$this->wrap($table).' '.$columns;
}
/**
* Create the individual column definitions for the table.
*
* @param Table $table
* @return array
*/
protected function columns(Table $table)
{
$columns = array();
foreach ($table->columns as $column)
{
// Each of the data type's have their own definition creation method,
// which is responsible for creating the SQL for the type. This lets
// us to keep the syntax easy and fluent, while translating the
// types to the types used by the database.
$sql = $this->wrap($column).' '.$this->type($column);
$elements = array('incrementer', 'nullable', 'defaults');
foreach ($elements as $element)
{
$sql .= $this->$element($table, $column);
}
$columns[] = $sql;
}
return $columns;
}
/**
* Get the SQL syntax for indicating if a column is nullable.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function nullable(Table $table, Fluent $column)
{
return ($column->nullable) ? ' NULL' : ' NOT NULL';
}
/**
* Get the SQL syntax for specifying a default value on a column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function defaults(Table $table, Fluent $column)
{
if ( ! is_null($column->default))
{
return " DEFAULT '".$column->default."'";
}
}
/**
* Get the SQL syntax for defining an auto-incrementing column.
*
* @param Table $table
* @param Fluent $column
* @return string
*/
protected function incrementer(Table $table, Fluent $column)
{
if ($column->type == 'integer' and $column->increment)
{
return ' IDENTITY PRIMARY KEY';
}
}
/**
* Generate the SQL statement for creating a primary key.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function primary(Table $table, Fluent $command)
{
$name = $command->name;
$columns = $this->columnize($columns);
return 'ALTER TABLE '.$this->wrap($table)." ADD CONSTRAINT {$name} PRIMARY KEY ({$columns})";
}
/**
* Generate the SQL statement for creating a unique index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function unique(Table $table, Fluent $command)
{
return $this->key($table, $command, true);
}
/**
* Generate the SQL statement for creating a full-text index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function fulltext(Table $table, Fluent $command)
{
$columns = $this->columnize($command->columns);
// SQL Server requires the creation of a full-text "catalog" before
// creating a full-text index, so we'll first create the catalog
// then add another statement for the index. The catalog will
// be updated automatically by the server.
$sql[] = "CREATE FULLTEXT CATALOG {$command->catalog}";
$create = "CREATE FULLTEXT INDEX ON ".$this->wrap($table)." ({$columns}) ";
// Full-text indexes must specify a unique, non-nullable column as
// the index "key" and this should have been created manually by
// the developer in a separate column addition command, so we
// can just specify it in this statement.
$sql[] = $create .= "KEY INDEX {$command->key} ON {$command->catalog}";
return $sql;
}
/**
* Generate the SQL statement for creating a regular index.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function index(Table $table, Fluent $command)
{
return $this->key($table, $command);
}
/**
* Generate the SQL statement for creating a new index.
*
* @param Table $table
* @param Fluent $command
* @param bool $unique
* @return string
*/
protected function key(Table $table, Fluent $command, $unique = false)
{
$columns = $this->columnize($command->columns);
$create = ($unique) ? 'CREATE UNIQUE' : 'CREATE';
return $create." INDEX {$command->name} ON ".$this->wrap($table)." ({$columns})";
}
/**
* Generate the SQL statement for a drop table command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop(Table $table, Fluent $command)
{
return 'DROP TABLE '.$this->wrap($table);
}
/**
* Generate the SQL statement for a drop column command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_column(Table $table, Fluent $command)
{
$columns = array_map(array($this, 'wrap'), $command->columns);
// Once we the array of column names, we need to add "drop" to the
// front of each column, then we'll concatenate the columns using
// commas and generate the alter statement SQL.
$columns = implode(', ', array_map(function($column)
{
return 'DROP '.$column;
}, $columns));
return 'ALTER TABLE '.$this->wrap($table).' '.$columns;
}
/**
* Generate the SQL statement for a drop primary key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_primary(Table $table, Fluent $command)
{
return 'ALTER TABLE '.$this->wrap($table).' DROP CONSTRAINT '.$command->name;
}
/**
* Generate the SQL statement for a drop unqiue key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_unique(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop full-text key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_fulltext(Table $table, Fluent $command)
{
$sql[] = "DROP FULLTEXT INDEX ".$command->name;
$sql[] = "DROP FULLTEXT CATALOG ".$command->catalog;
return $sql;
}
/**
* Generate the SQL statement for a drop index command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
public function drop_index(Table $table, Fluent $command)
{
return $this->drop_key($table, $command);
}
/**
* Generate the SQL statement for a drop key command.
*
* @param Table $table
* @param Fluent $command
* @return string
*/
protected function drop_key(Table $table, Fluent $command)
{
return "DROP INDEX {$command->name} ON ".$this->wrap($table);
}
/**
* Generate the data-type definition for a string.
*
* @param Fluent $column
* @return string
*/
protected function type_string(Fluent $column)
{
return 'NVARCHAR('.$column->length.')';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_integer(Fluent $column)
{
return 'INT';
}
/**
* Generate the data-type definition for an integer.
*
* @param Fluent $column
* @return string
*/
protected function type_float(Fluent $column)
{
return 'FLOAT';
}
/**
* Generate the data-type definition for a boolean.
*
* @param Fluent $column
* @return string
*/
protected function type_boolean(Fluent $column)
{
return 'TINYINT';
}
/**
* Generate the data-type definition for a date.
*
* @param Fluent $column
* @return string
*/
protected function type_date(Fluent $column)
{
return 'DATETIME';
}
/**
* Generate the data-type definition for a timestamp.
*
* @param Fluent $column
* @return string
*/
protected function type_timestamp(Fluent $column)
{
return 'TIMESTAMP';
}
/**
* Generate the data-type definition for a text column.
*
* @param Fluent $column
* @return string
*/
protected function type_text(Fluent $column)
{
return 'NVARCHAR(MAX)';
}
/**
* Generate the data-type definition for a blob.
*
* @param Fluent $column
* @return string
*/
protected function type_blob(Fluent $column)
{
return 'VARBINARY(MAX)';
}
}

View File

@@ -0,0 +1,371 @@
<?php namespace Laravel\Database\Schema;
use Laravel\Fluent;
class Table {
/**
* The database table name.
*
* @var string
*/
public $name;
/**
* The database connection that should be used.
*
* @var string
*/
public $connection;
/**
* The engine that should be used for the table.
*
* @var string
*/
public $engine;
/**
* The columns that should be added to the table.
*
* @var array
*/
public $columns = array();
/**
* The commands that should be executed on the table.
*
* @var array
*/
public $commands = array();
/**
* Create a new schema table instance.
*
* @param string $name
* @return void
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* Indicate that the table should be created.
*
* @return Fluent
*/
public function create()
{
return $this->command(__FUNCTION__);
}
/**
* Create a new primary key on the table.
*
* @param string|array $columns
* @param string $name
* @return Fluent
*/
public function primary($columns, $name = null)
{
return $this->key(__FUNCTION__, $columns, $name);
}
/**
* Create a new unique index on the table.
*
* @param string|array $columns
* @param string $name
* @return Fluent
*/
public function unique($columns, $name = null)
{
return $this->key(__FUNCTION__, $columns, $name);
}
/**
* Create a new full-text index on the table.
*
* @param string|array $columns
* @param string $name
* @return Fluent
*/
public function fulltext($columns, $name = null)
{
return $this->key(__FUNCTION__, $columns, $name);
}
/**
* Create a new index on the table.
*
* @param string|array
*/
public function index($columns, $name = null)
{
return $this->key(__FUNCTION__, $columns, $name);
}
/**
* Create a command for creating any index.
*
* @param string $type
* @param string|array $columns
* @param string $name
* @return Fluent
*/
public function key($type, $columns, $name = null)
{
$parameters = array('name' => $name, 'columns' => (array) $columns);
return $this->command($type, $parameters);
}
/**
* Drop the database table.
*
* @return Fluent
*/
public function drop()
{
return $this->command(__FUNCTION__);
}
/**
* Drop a column from the table.
*
* @param string|array $columns
* @return void
*/
public function drop_column($columns)
{
return $this->command(__FUNCTION__, array('columns' => (array) $columns));
}
/**
* Drop a primary key from the table.
*
* @param string $name
* @return void
*/
public function drop_primary($name)
{
return $this->drop_key(__FUNCTION__, $name);
}
/**
* Drop a unique index from the table.
*
* @param string $name
* @return void
*/
public function drop_unique($name)
{
return $this->drop_key(__FUNCTION__, $name);
}
/**
* Drop a full-text index from the table.
*
* @param string $name
* @return void
*/
public function drop_fulltext($name)
{
return $this->drop_key(__FUNCTION__, $name);
}
/**
* Drop an index from the table.
*
* @param string $name
* @return void
*/
public function drop_index($name)
{
return $this->drop_key(__FUNCTION__, $name);
}
/**
* Create a command to drop any type of index.
*
* @param string $type
* @param string $name
* @return Fluent
*/
protected function drop_key($type, $name)
{
return $this->command($type, array('name' => $name));
}
/**
* Add an auto-incrementing integer to the table.
*
* @param string $name
* @return Fluent
*/
public function increments($name)
{
return $this->integer($name, true);
}
/**
* Add a string column to the table.
*
* @param string $name
* @param int $length
* @return Fluent
*/
public function string($name, $length = 200)
{
return $this->column(__FUNCTION__, compact('name', 'length'));
}
/**
* Add an integer column to the table.
*
* @param string $name
* @param bool $increment
* @return Fluent
*/
public function integer($name, $increment = false)
{
return $this->column(__FUNCTION__, compact('name', 'increment'));
}
/**
* Add a float column to the table.
*
* @param string $name
* @param bool $increment
* @return Fluent
*/
public function float($name)
{
return $this->column(__FUNCTION__, compact('name'));
}
/**
* Add a boolean column to the table.
*
* @param string $name
* @return Fluent
*/
public function boolean($name)
{
return $this->column(__FUNCTION__, compact('name'));
}
/**
* Create date-time columns for creation and update timestamps.
*
* @return void
*/
public function timestamps()
{
$this->date('created_at');
$this->date('updated_at');
}
/**
* Add a date-time column to the table.
*
* @param string $name
* @return Fluent
*/
public function date($name)
{
return $this->column(__FUNCTION__, compact('name'));
}
/**
* Add a timestamp column to the table.
*
* @param string $name
* @return Fluent
*/
public function timestamp($name)
{
return $this->column(__FUNCTION__, compact('name'));
}
/**
* Add a text column to the table.
*
* @param string $name
* @return Fluent
*/
public function text($name)
{
return $this->column(__FUNCTION__, compact('name'));
}
/**
* Add a blob column to the table.
*
* @param string $name
* @return Fluent
*/
public function blob($name)
{
return $this->column(__FUNCTION__, compact('name'));
}
/**
* Set the database connection for the table operation.
*
* @param string $connection
* @return void
*/
public function on($connection)
{
$this->connection = $connection;
}
/**
* Determine if the schema table has a creation command.
*
* @return bool
*/
public function creating()
{
return ! is_null(array_first($this->commands, function($key, $value)
{
return $value->type == 'create';
}));
}
/**
* Create a new fluent command instance.
*
* @param string $type
* @param array $parameters
* @return Fluent
*/
protected function command($type, $parameters = array())
{
$parameters = array_merge(compact('type'), $parameters);
$this->commands[] = new Fluent($parameters);
return end($this->commands);
}
/**
* Create a new fluent column instance.
*
* @param string $type
* @param array $parameters
* @return Fluent
*/
protected function column($type, $parameters = array())
{
$parameters = array_merge(compact('type'), $parameters);
$this->columns[] = new Fluent($parameters);
return end($this->columns);
}
}