diff --git a/Capsule/Manager.php b/Capsule/Manager.php index 1b01b0177..aa272faec 100755 --- a/Capsule/Manager.php +++ b/Capsule/Manager.php @@ -1,32 +1,33 @@ setupContainer($container); + $this->setupContainer($container ?: new Container); // Once we have the container setup, we will setup the default configuration // options in the container "config" binding. This will make the database @@ -36,19 +37,6 @@ public function __construct(Container $container = null) $this->setupManager(); } - /** - * Setup the IoC container instance. - * - * @param \Illuminate\Container\Container $container - * @return void - */ - protected function setupContainer($container) - { - $this->container = $container ?: new Container; - - $this->container->instance('config', new Fluent); - } - /** * Setup the default database configuration options. * @@ -156,7 +144,7 @@ public function bootEloquent() * Set the fetch mode for the database connections. * * @param int $fetchMode - * @return \Illuminate\Database\Capsule\Manager + * @return $this */ public function setFetchMode($fetchMode) { @@ -166,19 +154,19 @@ public function setFetchMode($fetchMode) } /** - * Make this capsule instance available globally. + * Get the database manager instance. * - * @return void + * @return \Illuminate\Database\DatabaseManager */ - public function setAsGlobal() + public function getDatabaseManager() { - static::$instance = $this; + return $this->manager; } /** * Get the current event dispatcher instance. * - * @return \Illuminate\Events\Dispatcher + * @return \Illuminate\Contracts\Events\Dispatcher */ public function getEventDispatcher() { @@ -191,7 +179,7 @@ public function getEventDispatcher() /** * Set the event dispatcher instance to be used by connections. * - * @param \Illuminate\Events\Dispatcher $dispatcher + * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher * @return void */ public function setEventDispatcher(Dispatcher $dispatcher) @@ -199,51 +187,6 @@ public function setEventDispatcher(Dispatcher $dispatcher) $this->container->instance('events', $dispatcher); } - /** - * Get the current cache manager instance. - * - * @return \Illuminate\Cache\Manager - */ - public function getCacheManager() - { - if ($this->container->bound('cache')) - { - return $this->container['cache']; - } - } - - /** - * Set the cache manager to be used by connections. - * - * @param \Illuminate\Cache\CacheManager $cache - * @return void - */ - public function setCacheManager(CacheManager $cache) - { - $this->container->instance('cache', $cache); - } - - /** - * Get the IoC container instance. - * - * @return \Illuminate\Container\Container - */ - public function getContainer() - { - return $this->container; - } - - /** - * Set the IoC container instance. - * - * @param \Illuminate\Container\Container $container - * @return void - */ - public function setContainer(Container $container) - { - $this->container = $container; - } - /** * Dynamically pass methods to the default connection. * @@ -256,4 +199,4 @@ public static function __callStatic($method, $parameters) return call_user_func_array(array(static::connection(), $method), $parameters); } -} \ No newline at end of file +} diff --git a/Connection.php b/Connection.php index b5ff3fadd..f58e5510c 100755 --- a/Connection.php +++ b/Connection.php @@ -3,6 +3,10 @@ use PDO; use Closure; use DateTime; +use Exception; +use LogicException; +use RuntimeException; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Query\Processors\Processor; use Doctrine\DBAL\Connection as DoctrineConnection; @@ -22,6 +26,13 @@ class Connection implements ConnectionInterface { */ protected $readPdo; + /** + * The reconnector instance for the connection. + * + * @var callable + */ + protected $reconnector; + /** * The query grammar implementation. * @@ -46,24 +57,10 @@ class Connection implements ConnectionInterface { /** * The event dispatcher instance. * - * @var \Illuminate\Events\Dispatcher + * @var \Illuminate\Contracts\Events\Dispatcher */ protected $events; - /** - * The paginator environment instance. - * - * @var \Illuminate\Pagination\Paginator - */ - protected $paginator; - - /** - * The cache manager instance. - * - * @var \Illuminate\Cache\CacheManger - */ - protected $cache; - /** * The default fetch mode of the connection. * @@ -72,7 +69,7 @@ class Connection implements ConnectionInterface { protected $fetchMode = PDO::FETCH_ASSOC; /** - * The number of active transasctions. + * The number of active transactions. * * @var int */ @@ -90,7 +87,7 @@ class Connection implements ConnectionInterface { * * @var bool */ - protected $loggingQueries = true; + protected $loggingQueries = false; /** * Indicates if the connection is in a "dry run". @@ -123,10 +120,10 @@ class Connection implements ConnectionInterface { /** * Create a new database connection instance. * - * @param PDO $pdo - * @param string $database - * @param string $tablePrefix - * @param array $config + * @param \PDO $pdo + * @param string $database + * @param string $tablePrefix + * @param array $config * @return void */ public function __construct(PDO $pdo, $database = '', $tablePrefix = '', array $config = array()) @@ -266,16 +263,29 @@ public function selectOne($query, $bindings = array()) * @param array $bindings * @return array */ - public function select($query, $bindings = array()) + public function selectFromWriteConnection($query, $bindings = array()) { - return $this->run($query, $bindings, function($me, $query, $bindings) + return $this->select($query, $bindings, false); + } + + /** + * Run a select statement against the database. + * + * @param string $query + * @param array $bindings + * @param bool $useReadPdo + * @return array + */ + public function select($query, $bindings = array(), $useReadPdo = true) + { + return $this->run($query, $bindings, function($me, $query, $bindings) use ($useReadPdo) { if ($me->pretending()) return array(); // For select statements, we'll simply execute the query and return an array // of the database result set. Each element in the array will be a single // row from the database table, and will either be an array or objects. - $statement = $me->getReadPdo()->prepare($query); + $statement = $this->getPdoForSelect($useReadPdo)->prepare($query); $statement->execute($me->prepareBindings($bindings)); @@ -283,6 +293,17 @@ public function select($query, $bindings = array()) }); } + /** + * Get the PDO connection to use for a select query. + * + * @param bool $useReadPdo + * @return \PDO + */ + protected function getPdoForSelect($useReadPdo = true) + { + return $useReadPdo ? $this->getReadPdo() : $this->getPdo(); + } + /** * Run an insert statement against the database. * @@ -370,7 +391,7 @@ public function affectingStatement($query, $bindings = array()) */ public function unprepared($query) { - return $this->run($query, array(), function($me, $query, $bindings) + return $this->run($query, array(), function($me, $query) { if ($me->pretending()) return true; @@ -409,7 +430,7 @@ public function prepareBindings(array $bindings) /** * Execute a Closure within a transaction. * - * @param Closure $callback + * @param \Closure $callback * @return mixed * * @throws \Exception @@ -431,7 +452,7 @@ public function transaction(Closure $callback) // If we catch an exception, we will roll back so nothing gets messed // up in the database. Then we'll re-throw the exception so it can // be handled how the developer sees fit for their applications. - catch (\Exception $e) + catch (Exception $e) { $this->rollBack(); @@ -454,6 +475,8 @@ public function beginTransaction() { $this->pdo->beginTransaction(); } + + $this->fireConnectionEvent('beganTransaction'); } /** @@ -466,6 +489,8 @@ public function commit() if ($this->transactions == 1) $this->pdo->commit(); --$this->transactions; + + $this->fireConnectionEvent('committed'); } /** @@ -485,12 +510,24 @@ public function rollBack() { --$this->transactions; } + + $this->fireConnectionEvent('rollingBack'); + } + + /** + * Get the number of active transactions. + * + * @return int + */ + public function transactionLevel() + { + return $this->transactions; } /** * Execute the given callback in "dry run" mode. * - * @param Closure $callback + * @param \Closure $callback * @return array */ public function pretend(Closure $callback) @@ -512,17 +549,55 @@ public function pretend(Closure $callback) /** * Run a SQL statement and log its execution context. * - * @param string $query - * @param array $bindings - * @param Closure $callback + * @param string $query + * @param array $bindings + * @param \Closure $callback * @return mixed * - * @throws QueryException + * @throws \Illuminate\Database\QueryException */ protected function run($query, $bindings, Closure $callback) { + $this->reconnectIfMissingConnection(); + $start = microtime(true); + // Here we will run this query. If an exception occurs we'll determine if it was + // caused by a connection that has been lost. If that is the cause, we'll try + // to re-establish connection and re-run the query with a fresh connection. + try + { + $result = $this->runQueryCallback($query, $bindings, $callback); + } + catch (QueryException $e) + { + $result = $this->tryAgainIfCausedByLostConnection( + $e, $query, $bindings, $callback + ); + } + + // Once we have run the query we will calculate the time that it took to run and + // then log the query, bindings, and execution time so we will report them on + // the event that the developer needs them. We'll log time in milliseconds. + $time = $this->getElapsedTime($start); + + $this->logQuery($query, $bindings, $time); + + return $result; + } + + /** + * Run a SQL statement. + * + * @param string $query + * @param array $bindings + * @param \Closure $callback + * @return mixed + * + * @throws \Illuminate\Database\QueryException + */ + protected function runQueryCallback($query, $bindings, Closure $callback) + { // To execute the statement, we'll simply call the callback, which will actually // run the SQL against the PDO connection. Then we can calculate the time it // took to execute and log the query SQL, bindings and time in our memory. @@ -534,19 +609,88 @@ protected function run($query, $bindings, Closure $callback) // If an exception occurs when attempting to run a query, we'll format the error // message to include the bindings with SQL, which will make this exception a // lot more helpful to the developer instead of just the database's errors. - catch (\Exception $e) + catch (Exception $e) { - throw new QueryException($query, $bindings, $e); + throw new QueryException( + $query, $this->prepareBindings($bindings), $e + ); } - // Once we have run the query we will calculate the time that it took to run and - // then log the query, bindings, and execution time so we will report them on - // the event that the developer needs them. We'll log time in milliseconds. - $time = $this->getElapsedTime($start); + return $result; + } - $this->logQuery($query, $bindings, $time); + /** + * Handle a query exception that occurred during query execution. + * + * @param \Illuminate\Database\QueryException $e + * @param string $query + * @param array $bindings + * @param \Closure $callback + * @return mixed + * + * @throws \Illuminate\Database\QueryException + */ + protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback) + { + if ($this->causedByLostConnection($e)) + { + $this->reconnect(); - return $result; + return $this->runQueryCallback($query, $bindings, $callback); + } + + throw $e; + } + + /** + * Determine if the given exception was caused by a lost connection. + * + * @param \Illuminate\Database\QueryException + * @return bool + */ + protected function causedByLostConnection(QueryException $e) + { + return str_contains($e->getPrevious()->getMessage(), 'server has gone away'); + } + + /** + * Disconnect from the underlying PDO connection. + * + * @return void + */ + public function disconnect() + { + $this->setPdo(null)->setReadPdo(null); + } + + /** + * Reconnect to the database. + * + * @return void + * + * @throws \LogicException + */ + public function reconnect() + { + if (is_callable($this->reconnector)) + { + return call_user_func($this->reconnector, $this); + } + + throw new LogicException("Lost connection and no reconnector available."); + } + + /** + * Reconnect to the database if a PDO connection is missing. + * + * @return void + */ + protected function reconnectIfMissingConnection() + { + if (is_null($this->getPdo()) || is_null($this->getReadPdo())) + { + $this->reconnect(); + } } /** @@ -554,7 +698,7 @@ protected function run($query, $bindings, Closure $callback) * * @param string $query * @param array $bindings - * @param $time + * @param float|null $time * @return void */ public function logQuery($query, $bindings, $time = null) @@ -572,7 +716,7 @@ public function logQuery($query, $bindings, $time = null) /** * Register a database query listener with the connection. * - * @param Closure $callback + * @param \Closure $callback * @return void */ public function listen(Closure $callback) @@ -583,6 +727,20 @@ public function listen(Closure $callback) } } + /** + * Fire an event for this connection. + * + * @param string $event + * @return void + */ + protected function fireConnectionEvent($event) + { + if (isset($this->events)) + { + $this->events->fire('connection.'.$this->getName().'.'.$event, $this); + } + } + /** * Get the elapsed time since a given starting point. * @@ -635,7 +793,7 @@ public function getDoctrineConnection() /** * Get the current PDO connection. * - * @return PDO + * @return \PDO */ public function getPdo() { @@ -645,21 +803,26 @@ public function getPdo() /** * Get the current PDO connection used for reading. * - * @return PDO + * @return \PDO */ public function getReadPdo() { + if ($this->transactions >= 1) return $this->getPdo(); + return $this->readPdo ?: $this->pdo; } /** * Set the PDO connection. * - * @param PDO $pdo - * @return \Illuminate\Database\Connection + * @param \PDO|null $pdo + * @return $this */ - public function setPdo(PDO $pdo) + public function setPdo($pdo) { + if ($this->transactions >= 1) + throw new RuntimeException("Can't swap PDO instance while within transaction."); + $this->pdo = $pdo; return $this; @@ -668,16 +831,29 @@ public function setPdo(PDO $pdo) /** * Set the PDO connection used for reading. * - * @param PDO $pdo - * @return \Illuminate\Database\Connection + * @param \PDO|null $pdo + * @return $this */ - public function setReadPdo(PDO $pdo) + public function setReadPdo($pdo) { $this->readPdo = $pdo; return $this; } + /** + * Set the reconnect instance on the connection. + * + * @param callable $reconnector + * @return $this + */ + public function setReconnector(callable $reconnector) + { + $this->reconnector = $reconnector; + + return $this; + } + /** * Get the database connection name. * @@ -775,7 +951,7 @@ public function setPostProcessor(Processor $processor) /** * Get the event dispatcher used by the connection. * - * @return \Illuminate\Events\Dispatcher + * @return \Illuminate\Contracts\Events\Dispatcher */ public function getEventDispatcher() { @@ -785,66 +961,14 @@ public function getEventDispatcher() /** * Set the event dispatcher instance on the connection. * - * @param \Illuminate\Events\Dispatcher + * @param \Illuminate\Contracts\Events\Dispatcher * @return void */ - public function setEventDispatcher(\Illuminate\Events\Dispatcher $events) + public function setEventDispatcher(Dispatcher $events) { $this->events = $events; } - /** - * Get the paginator environment instance. - * - * @return \Illuminate\Pagination\Environment - */ - public function getPaginator() - { - if ($this->paginator instanceof Closure) - { - $this->paginator = call_user_func($this->paginator); - } - - return $this->paginator; - } - - /** - * Set the pagination environment instance. - * - * @param \Illuminate\Pagination\Environment|\Closure $paginator - * @return void - */ - public function setPaginator($paginator) - { - $this->paginator = $paginator; - } - - /** - * Get the cache manager instance. - * - * @return \Illuminate\Cache\CacheManager - */ - public function getCacheManager() - { - if ($this->cache instanceof Closure) - { - $this->cache = call_user_func($this->cache); - } - - return $this->cache; - } - - /** - * Set the cache manager instance on the connection. - * - * @param \Illuminate\Cache\CacheManager|\Closure $cache - * @return void - */ - public function setCacheManager($cache) - { - $this->cache = $cache; - } - /** * Determine if the connection in a "dry run". * @@ -916,6 +1040,16 @@ public function disableQueryLog() $this->loggingQueries = false; } + /** + * Determine whether we're logging queries. + * + * @return bool + */ + public function logging() + { + return $this->loggingQueries; + } + /** * Get the name of the connected database. * diff --git a/ConnectionInterface.php b/ConnectionInterface.php index fb5282a02..48d69b334 100755 --- a/ConnectionInterface.php +++ b/ConnectionInterface.php @@ -4,6 +4,22 @@ interface ConnectionInterface { + /** + * Begin a fluent query against a database table. + * + * @param string $table + * @return \Illuminate\Database\Query\Builder + */ + public function table($table); + + /** + * Get a new raw query expression. + * + * @param mixed $value + * @return \Illuminate\Database\Query\Expression + */ + public function raw($value); + /** * Run a select statement and return a single result. * @@ -58,12 +74,75 @@ public function delete($query, $bindings = array()); */ public function statement($query, $bindings = array()); + /** + * Run an SQL statement and get the number of rows affected. + * + * @param string $query + * @param array $bindings + * @return int + */ + public function affectingStatement($query, $bindings = array()); + + /** + * Run a raw, unprepared query against the PDO connection. + * + * @param string $query + * @return bool + */ + public function unprepared($query); + + /** + * Prepare the query bindings for execution. + * + * @param array $bindings + * @return array + */ + public function prepareBindings(array $bindings); + /** * Execute a Closure within a transaction. * - * @param Closure $callback + * @param \Closure $callback * @return mixed + * + * @throws \Exception */ public function transaction(Closure $callback); -} \ No newline at end of file + /** + * Start a new database transaction. + * + * @return void + */ + public function beginTransaction(); + + /** + * Commit the active database transaction. + * + * @return void + */ + public function commit(); + + /** + * Rollback the active database transaction. + * + * @return void + */ + public function rollBack(); + + /** + * Get the number of active transactions. + * + * @return int + */ + public function transactionLevel(); + + /** + * Execute the given callback in "dry run" mode. + * + * @param \Closure $callback + * @return array + */ + public function pretend(Closure $callback); + +} diff --git a/ConnectionResolver.php b/ConnectionResolver.php index 4312475ed..79469b20e 100755 --- a/ConnectionResolver.php +++ b/ConnectionResolver.php @@ -34,7 +34,7 @@ public function __construct(array $connections = array()) * Get a database connection instance. * * @param string $name - * @return \Illuminate\Database\Connection + * @return \Illuminate\Database\ConnectionInterface */ public function connection($name = null) { @@ -47,10 +47,10 @@ public function connection($name = null) * Add a connection to the resolver. * * @param string $name - * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\ConnectionInterface $connection * @return void */ - public function addConnection($name, Connection $connection) + public function addConnection($name, ConnectionInterface $connection) { $this->connections[$name] = $connection; } @@ -87,4 +87,4 @@ public function setDefaultConnection($name) $this->default = $name; } -} \ No newline at end of file +} diff --git a/ConnectionResolverInterface.php b/ConnectionResolverInterface.php index 7e9cfd651..46abdc037 100755 --- a/ConnectionResolverInterface.php +++ b/ConnectionResolverInterface.php @@ -25,4 +25,4 @@ public function getDefaultConnection(); */ public function setDefaultConnection($name); -} \ No newline at end of file +} diff --git a/Connectors/ConnectionFactory.php b/Connectors/ConnectionFactory.php index 5a27557c4..37dd69fca 100755 --- a/Connectors/ConnectionFactory.php +++ b/Connectors/ConnectionFactory.php @@ -1,25 +1,26 @@ createReadWriteConnection($config); } - else - { - return $this->createSingleConnection($config); - } + + return $this->createSingleConnection($config); } /** @@ -116,7 +115,7 @@ protected function getWriteConfig(array $config) /** * Get a read / write level configuration. * - * @param array $config + * @param array $config * @param string $type * @return array */ @@ -126,10 +125,8 @@ protected function getReadWriteConfig(array $config, $type) { return $config[$type][array_rand($config[$type])]; } - else - { - return $config[$type]; - } + + return $config[$type]; } /** @@ -168,7 +165,12 @@ public function createConnector(array $config) { if ( ! isset($config['driver'])) { - throw new \InvalidArgumentException("A driver must be specified."); + throw new InvalidArgumentException("A driver must be specified."); + } + + if ($this->container->bound($key = "db.connector.{$config['driver']}")) + { + return $this->container->make($key); } switch ($config['driver']) @@ -186,22 +188,22 @@ public function createConnector(array $config) return new SqlServerConnector; } - throw new \InvalidArgumentException("Unsupported driver [{$config['driver']}]"); + throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]"); } /** * Create a new connection instance. * - * @param string $driver - * @param PDO $connection - * @param string $database - * @param string $prefix - * @param array $config + * @param string $driver + * @param \PDO $connection + * @param string $database + * @param string $prefix + * @param array $config * @return \Illuminate\Database\Connection * * @throws \InvalidArgumentException */ - protected function createConnection($driver, PDO $connection, $database, $prefix = '', $config = null) + protected function createConnection($driver, PDO $connection, $database, $prefix = '', array $config = array()) { if ($this->container->bound($key = "db.connection.{$driver}")) { @@ -223,7 +225,7 @@ protected function createConnection($driver, PDO $connection, $database, $prefix return new SqlServerConnection($connection, $database, $prefix, $config); } - throw new \InvalidArgumentException("Unsupported driver [$driver]"); + throw new InvalidArgumentException("Unsupported driver [$driver]"); } } diff --git a/Connectors/Connector.php b/Connectors/Connector.php index bb24034f4..6c388e068 100755 --- a/Connectors/Connector.php +++ b/Connectors/Connector.php @@ -10,11 +10,11 @@ class Connector { * @var array */ protected $options = array( - PDO::ATTR_CASE => PDO::CASE_NATURAL, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, - PDO::ATTR_STRINGIFY_FETCHES => false, - PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, ); /** @@ -36,7 +36,7 @@ public function getOptions(array $config) * @param string $dsn * @param array $config * @param array $options - * @return PDO + * @return \PDO */ public function createConnection($dsn, array $config, array $options) { @@ -68,4 +68,4 @@ public function setDefaultOptions(array $options) $this->options = $options; } -} \ No newline at end of file +} diff --git a/Connectors/ConnectorInterface.php b/Connectors/ConnectorInterface.php index c734f9dbe..c2c76a5fd 100755 --- a/Connectors/ConnectorInterface.php +++ b/Connectors/ConnectorInterface.php @@ -6,8 +6,8 @@ interface ConnectorInterface { * Establish a database connection. * * @param array $config - * @return PDO + * @return \PDO */ public function connect(array $config); -} \ No newline at end of file +} diff --git a/Connectors/MySqlConnector.php b/Connectors/MySqlConnector.php index fbdadfb0d..46ad0a799 100755 --- a/Connectors/MySqlConnector.php +++ b/Connectors/MySqlConnector.php @@ -6,19 +6,24 @@ class MySqlConnector extends Connector implements ConnectorInterface { * Establish a database connection. * * @param array $config - * @return PDO + * @return \PDO */ public function connect(array $config) { $dsn = $this->getDsn($config); + $options = $this->getOptions($config); + // We need to grab the PDO options that should be used while making the brand // new connection instance. The PDO options control various aspects of the // connection's behavior, and some might be specified by the developers. - $options = $this->getOptions($config); - $connection = $this->createConnection($dsn, $config, $options); + if (isset($config['unix_socket'])) + { + $connection->exec("use {$config['database']};"); + } + $collation = $config['collation']; // Next we will set the "names" and "collation" on the clients connections so @@ -26,7 +31,8 @@ public function connect(array $config) // is set on the server but needs to be set here on this client objects. $charset = $config['charset']; - $names = "set names '$charset' collate '$collation'"; + $names = "set names '$charset'". + ( ! is_null($collation) ? " collate '$collation'" : ''); $connection->prepare($names)->execute(); @@ -42,34 +48,54 @@ public function connect(array $config) } /** - * Create a DSN string from a configuration. + * Create a DSN string from a configuration. Chooses socket or host/port based on + * the 'unix_socket' config value * * @param array $config * @return string */ protected function getDsn(array $config) { - // First we will create the basic DSN setup as well as the port if it is in - // in the configuration options. This will give us the basic DSN we will - // need to establish the PDO connections and return them back for use. - extract($config); + return $this->configHasSocket($config) ? $this->getSocketDsn($config) : $this->getHostDsn($config); + } - $dsn = "mysql:host={$host};dbname={$database}"; + /** + * Determine if the given configuration array has a UNIX socket value. + * + * @param array $config + * @return bool + */ + protected function configHasSocket(array $config) + { + return isset($config['unix_socket']) && ! empty($config['unix_socket']); + } - if (isset($config['port'])) - { - $dsn .= ";port={$port}"; - } + /** + * Get the DSN string for a socket configuration. + * + * @param array $config + * @return string + */ + protected function getSocketDsn(array $config) + { + extract($config); - // Sometimes the developer may specify the specific UNIX socket that should - // be used. If that is the case we will add that option to the string we - // have created so that it gets utilized while the connection is made. - if (isset($config['unix_socket'])) - { - $dsn .= ";unix_socket={$config['unix_socket']}"; - } + return "mysql:unix_socket={$config['unix_socket']};dbname={$database}"; + } + + /** + * Get the DSN string for a host / port configuration. + * + * @param array $config + * @return string + */ + protected function getHostDsn(array $config) + { + extract($config); - return $dsn; + return isset($config['port']) + ? "mysql:host={$host};port={$port};dbname={$database}" + : "mysql:host={$host};dbname={$database}"; } -} \ No newline at end of file +} diff --git a/Connectors/PostgresConnector.php b/Connectors/PostgresConnector.php index dfa7d3388..14b0f441e 100755 --- a/Connectors/PostgresConnector.php +++ b/Connectors/PostgresConnector.php @@ -10,18 +10,17 @@ class PostgresConnector extends Connector implements ConnectorInterface { * @var array */ protected $options = array( - PDO::ATTR_CASE => PDO::CASE_NATURAL, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, - PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, ); - /** * Establish a database connection. * * @param array $config - * @return PDO + * @return \PDO */ public function connect(array $config) { @@ -38,6 +37,16 @@ public function connect(array $config) $connection->prepare("set names '$charset'")->execute(); + // Next, we will check to see if a timezone has been specified in this config + // and if it has we will issue a statement to modify the timezone with the + // database. Setting this DB timezone is an optional configuration item. + if (isset($config['timezone'])) + { + $timezone = $config['timezone']; + + $connection->prepare("set time zone '$timezone'")->execute(); + } + // Unlike MySQL, Postgres allows the concept of "schema" and a default schema // may have been specified on the connections. If that is the case we will // set the default schema search paths to the specified database schema. @@ -76,7 +85,12 @@ protected function getDsn(array $config) $dsn .= ";port={$port}"; } + if (isset($config['sslmode'])) + { + $dsn .= ";sslmode={$sslmode}"; + } + return $dsn; } -} \ No newline at end of file +} diff --git a/Connectors/SQLiteConnector.php b/Connectors/SQLiteConnector.php index 5d9f75dc0..697a72b80 100755 --- a/Connectors/SQLiteConnector.php +++ b/Connectors/SQLiteConnector.php @@ -1,12 +1,14 @@ createConnection("sqlite:{$path}", $config, $options); } -} \ No newline at end of file +} diff --git a/Connectors/SqlServerConnector.php b/Connectors/SqlServerConnector.php index df3294d2a..c4d14c238 100755 --- a/Connectors/SqlServerConnector.php +++ b/Connectors/SqlServerConnector.php @@ -10,17 +10,17 @@ class SqlServerConnector extends Connector implements ConnectorInterface { * @var array */ protected $options = array( - PDO::ATTR_CASE => PDO::CASE_NATURAL, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, - PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, ); /** * Establish a database connection. * * @param array $config - * @return PDO + * @return \PDO */ public function connect(array $config) { @@ -37,25 +37,47 @@ public function connect(array $config) */ protected function getDsn(array $config) { - extract($config); - // First we will create the basic DSN setup as well as the port if it is in // in the configuration options. This will give us the basic DSN we will // need to establish the PDO connections and return them back for use. - $port = isset($config['port']) ? ','.$port : ''; - if (in_array('dblib', $this->getAvailableDrivers())) { - return "dblib:host={$host}{$port};dbname={$database}"; + return $this->getDblibDsn($config); } else { - $dbName = $database != '' ? ";Database={$database}" : ''; - - return "sqlsrv:Server={$host}{$port}{$dbName}"; + return $this->getSqlSrvDsn($config); } } + /** + * Get the DSN string for a DbLib connection. + * + * @param array $config + * @return string + */ + protected function getDblibDsn(array $config) + { + $port = isset($config['port']) ? ':'.$config['port'] : ''; + + return "dblib:host={$config['host']}{$port};dbname={$config['database']}"; + } + + /** + * Get the DSN string for a SqlSrv connection. + * + * @param array $config + * @return string + */ + protected function getSqlSrvDsn(array $config) + { + $port = isset($config['port']) ? ','.$config['port'] : ''; + + $dbName = $config['database'] != '' ? ";Database={$config['database']}" : ''; + + return "sqlsrv:Server={$config['host']}{$port}{$dbName}"; + } + /** * Get the available PDO drivers. * @@ -66,4 +88,4 @@ protected function getAvailableDrivers() return PDO::getAvailableDrivers(); } -} \ No newline at end of file +} diff --git a/Console/Migrations/BaseCommand.php b/Console/Migrations/BaseCommand.php index d4a3d5de6..f24024bb9 100755 --- a/Console/Migrations/BaseCommand.php +++ b/Console/Migrations/BaseCommand.php @@ -11,39 +11,7 @@ class BaseCommand extends Command { */ protected function getMigrationPath() { - $path = $this->input->getOption('path'); - - // First, we will check to see if a path option has been defined. If it has - // we will use the path relative to the root of this installation folder - // so that migrations may be run for any path within the applications. - if ( ! is_null($path)) - { - return $this->laravel['path.base'].'/'.$path; - } - - $package = $this->input->getOption('package'); - - // If the package is in the list of migration paths we received we will put - // the migrations in that path. Otherwise, we will assume the package is - // is in the package directories and will place them in that location. - if ( ! is_null($package)) - { - return $this->packagePath.'/'.$package.'/src/migrations'; - } - - $bench = $this->input->getOption('bench'); - - // Finally we will check for the workbench option, which is a shortcut into - // specifying the full path for a "workbench" project. Workbenches allow - // developers to develop packages along side a "standard" app install. - if ( ! is_null($bench)) - { - $path = "/workbench/{$bench}/src/migrations"; - - return $this->laravel['path.base'].$path; - } - - return $this->laravel['path'].'/database/migrations'; + return $this->laravel['path.database'].'/migrations'; } -} \ No newline at end of file +} diff --git a/Console/Migrations/InstallCommand.php b/Console/Migrations/InstallCommand.php index 9feb2aed1..d89c0c4af 100755 --- a/Console/Migrations/InstallCommand.php +++ b/Console/Migrations/InstallCommand.php @@ -23,14 +23,14 @@ class InstallCommand extends Command { /** * The repository instance. * - * @var \Illuminate\Database\Console\Migrations\MigrationRepositoryInterface + * @var \Illuminate\Database\Migrations\MigrationRepositoryInterface */ protected $repository; /** * Create a new migration install command instance. * - * @param \Illuminate\Database\Console\Migrations\MigrationRepositoryInterface $repository + * @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository * @return void */ public function __construct(MigrationRepositoryInterface $repository) @@ -66,4 +66,4 @@ protected function getOptions() ); } -} \ No newline at end of file +} diff --git a/Console/Migrations/MigrateCommand.php b/Console/Migrations/MigrateCommand.php index 5a06b8764..24db86d07 100755 --- a/Console/Migrations/MigrateCommand.php +++ b/Console/Migrations/MigrateCommand.php @@ -1,10 +1,13 @@ migrator = $migrator; - $this->packagePath = $packagePath; } /** @@ -53,6 +49,8 @@ public function __construct(Migrator $migrator, $packagePath) */ public function fire() { + if ( ! $this->confirmToProceed()) return; + $this->prepareDatabase(); // The pretend option can be used for "simulating" the migration and grabbing @@ -60,7 +58,17 @@ public function fire() // a database for real, which is helpful for double checking migrations. $pretend = $this->input->getOption('pretend'); - $path = $this->getMigrationPath(); + // Next, we will check to see if a path option has been defined. If it has + // we will use the path relative to the root of this installation folder + // so that migrations may be run for any path within the applications. + if ( ! is_null($path = $this->input->getOption('path'))) + { + $path = $this->laravel['path.base'].'/'.$path; + } + else + { + $path = $this->getMigrationPath(); + } $this->migrator->run($path, $pretend); @@ -77,7 +85,7 @@ public function fire() // a migration and a seed at the same time, as it is only this command. if ($this->input->getOption('seed')) { - $this->call('db:seed'); + $this->call('db:seed', ['--force' => true]); } } @@ -106,13 +114,11 @@ protected function prepareDatabase() protected function getOptions() { return array( - array('bench', null, InputOption::VALUE_OPTIONAL, 'The name of the workbench to migrate.', null), - array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), - array('path', null, InputOption::VALUE_OPTIONAL, 'The path to migration files.', null), + array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), - array('package', null, InputOption::VALUE_OPTIONAL, 'The package to migrate.', null), + array('path', null, InputOption::VALUE_OPTIONAL, 'The path of migrations files to be executed.'), array('pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'), @@ -120,4 +126,4 @@ protected function getOptions() ); } -} \ No newline at end of file +} diff --git a/Console/Migrations/MakeCommand.php b/Console/Migrations/MigrateMakeCommand.php old mode 100755 new mode 100644 similarity index 77% rename from Console/Migrations/MakeCommand.php rename to Console/Migrations/MigrateMakeCommand.php index 2ca9e7763..2aafca623 --- a/Console/Migrations/MakeCommand.php +++ b/Console/Migrations/MigrateMakeCommand.php @@ -1,17 +1,18 @@ creator = $creator; - $this->packagePath = $packagePath; + $this->composer = $composer; } /** @@ -64,19 +63,15 @@ public function fire() $table = $this->input->getOption('table'); $create = $this->input->getOption('create'); - - if ( ! $table && is_string($create)) - { - $table = $create; - } + if ( ! $table && is_string($create)) $table = $create; // Now we are ready to write the migration out to disk. Once we've written // the migration out, we will dump-autoload for the entire framework to // make sure that the migrations are registered by the class loaders. $this->writeMigration($name, $table, $create); - $this->call('dump-autoload'); + $this->composer->dumpAutoloads(); } /** @@ -116,14 +111,8 @@ protected function getArguments() protected function getOptions() { return array( - array('bench', null, InputOption::VALUE_OPTIONAL, 'The workbench the migration belongs to.', null), - array('create', null, InputOption::VALUE_OPTIONAL, 'The table to be created.'), - array('package', null, InputOption::VALUE_OPTIONAL, 'The package the migration belongs to.', null), - - array('path', null, InputOption::VALUE_OPTIONAL, 'Where to store the migration.', null), - array('table', null, InputOption::VALUE_OPTIONAL, 'The table to migrate.'), ); } diff --git a/Console/Migrations/RefreshCommand.php b/Console/Migrations/RefreshCommand.php index 330dc73a4..2adc6e82e 100755 --- a/Console/Migrations/RefreshCommand.php +++ b/Console/Migrations/RefreshCommand.php @@ -1,10 +1,13 @@ confirmToProceed()) return; + $database = $this->input->getOption('database'); - $this->call('migrate:reset', array('--database' => $database)); + $force = $this->input->getOption('force'); + + $this->call('migrate:reset', array( + '--database' => $database, '--force' => $force + )); // The refresh command is essentially just a brief aggregate of a few other of // the migration commands and just provides a convenient wrapper to execute - // them in succession. We'll also see if we need to res-eed the database. - $this->call('migrate', array('--database' => $database)); + // them in succession. We'll also see if we need to re-seed the database. + $this->call('migrate', array( + '--database' => $database, '--force' => $force + )); - if ($this->input->getOption('seed')) + if ($this->needsSeeding()) { - $this->call('db:seed', array('--database' => $database)); + $this->runSeeder($database); } } + /** + * Determine if the developer has requested database seeding. + * + * @return bool + */ + protected function needsSeeding() + { + return $this->option('seed') || $this->option('seeder'); + } + + /** + * Run the database seeder command. + * + * @param string $database + * @return void + */ + protected function runSeeder($database) + { + $class = $this->option('seeder') ?: 'DatabaseSeeder'; + + $this->call('db:seed', array('--database' => $database, '--class' => $class)); + } + /** * Get the console command options. * @@ -51,8 +85,12 @@ protected function getOptions() return array( array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), + array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), + array('seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run.'), + + array('seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder.'), ); } -} \ No newline at end of file +} diff --git a/Console/Migrations/ResetCommand.php b/Console/Migrations/ResetCommand.php index 386858daa..f81fa907d 100755 --- a/Console/Migrations/ResetCommand.php +++ b/Console/Migrations/ResetCommand.php @@ -1,11 +1,14 @@ confirmToProceed()) return; + $this->migrator->setConnection($this->input->getOption('database')); $pretend = $this->input->getOption('pretend'); @@ -77,8 +82,10 @@ protected function getOptions() return array( array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), + array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), + array('pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'), ); } -} \ No newline at end of file +} diff --git a/Console/Migrations/RollbackCommand.php b/Console/Migrations/RollbackCommand.php index 5d2ab4bec..c11198f6f 100755 --- a/Console/Migrations/RollbackCommand.php +++ b/Console/Migrations/RollbackCommand.php @@ -1,11 +1,14 @@ confirmToProceed()) return; + $this->migrator->setConnection($this->input->getOption('database')); $pretend = $this->input->getOption('pretend'); @@ -72,8 +77,10 @@ protected function getOptions() return array( array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use.'), + array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), + array('pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run.'), ); } -} \ No newline at end of file +} diff --git a/Console/Migrations/StatusCommand.php b/Console/Migrations/StatusCommand.php new file mode 100644 index 000000000..c575112f8 --- /dev/null +++ b/Console/Migrations/StatusCommand.php @@ -0,0 +1,82 @@ +migrator = $migrator; + } + + /** + * Execute the console command. + * + * @return void + */ + public function fire() + { + if ( ! $this->migrator->repositoryExists()) + { + return $this->error('No migrations found.'); + } + + $ran = $this->migrator->getRepository()->getRan(); + + $migrations = []; + + foreach ($this->getAllMigrationFiles() as $migration) + { + $migrations[] = in_array($migration, $ran) ? ['', $migration] : ['', $migration]; + } + + if (count($migrations) > 0) + { + $this->table(['Ran?', 'Migration'], $migrations); + } + else + { + $this->error('No migrations found'); + } + } + + /** + * Get all of the migration files. + * + * @return array + */ + protected function getAllMigrationFiles() + { + return $this->migrator->getMigrationFiles($this->getMigrationPath()); + } + +} diff --git a/Console/SeedCommand.php b/Console/SeedCommand.php index 7baf697ca..cba115b47 100755 --- a/Console/SeedCommand.php +++ b/Console/SeedCommand.php @@ -1,11 +1,14 @@ confirmToProceed()) return; + $this->resolver->setDefaultConnection($this->getDatabase()); $this->getSeeder()->run(); @@ -87,7 +92,9 @@ protected function getOptions() array('class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'DatabaseSeeder'), array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to seed'), + + array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'), ); } -} \ No newline at end of file +} diff --git a/DatabaseManager.php b/DatabaseManager.php index 82e6da7aa..45cde61b5 100755 --- a/DatabaseManager.php +++ b/DatabaseManager.php @@ -1,5 +1,7 @@ getDefaultConnection(); + list($name, $type) = $this->parseConnectionName($name); // If we haven't created this connection, we'll create it based on the config // provided in the application. Once we've created the connections we will @@ -62,6 +64,8 @@ public function connection($name = null) { $connection = $this->makeConnection($name); + $this->setPdoForType($connection, $type); + $this->connections[$name] = $this->prepare($connection); } @@ -69,20 +73,32 @@ public function connection($name = null) } /** - * Reconnect to the given database. + * Parse the connection into an array of the name and read / write type. * * @param string $name - * @return \Illuminate\Database\Connection + * @return array */ - public function reconnect($name = null) + protected function parseConnectionName($name) { $name = $name ?: $this->getDefaultConnection(); + return Str::endsWith($name, ['::read', '::write']) + ? explode('::', $name, 2) : [$name, null]; + } + + /** + * Disconnect from the given database and remove from local cache. + * + * @param string $name + * @return void + */ + public function purge($name = null) + { $this->disconnect($name); - return $this->connection($name); + unset($this->connections[$name]); } - + /** * Disconnect from the given database. * @@ -91,9 +107,43 @@ public function reconnect($name = null) */ public function disconnect($name = null) { - $name = $name ?: $this->getDefaultConnection(); + if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) + { + $this->connections[$name]->disconnect(); + } + } - unset($this->connections[$name]); + /** + * Reconnect to the given database. + * + * @param string $name + * @return \Illuminate\Database\Connection + */ + public function reconnect($name = null) + { + $this->disconnect($name = $name ?: $this->getDefaultConnection()); + + if ( ! isset($this->connections[$name])) + { + return $this->connection($name); + } + + return $this->refreshPdoConnections($name); + } + + /** + * Refresh the PDO connections on a given connection. + * + * @param string $name + * @return \Illuminate\Database\Connection + */ + protected function refreshPdoConnections($name) + { + $fresh = $this->makeConnection($name); + + return $this->connections[$name] + ->setPdo($fresh->getPdo()) + ->setReadPdo($fresh->getReadPdo()); } /** @@ -111,7 +161,7 @@ protected function makeConnection($name) // Closure and pass it the config allowing it to resolve the connection. if (isset($this->extensions[$name])) { - return call_user_func($this->extensions[$name], $config); + return call_user_func($this->extensions[$name], $config, $name); } $driver = $config['driver']; @@ -121,7 +171,7 @@ protected function makeConnection($name) // resolver for the drivers themselves which applies to all connections. if (isset($this->extensions[$driver])) { - return call_user_func($this->extensions[$driver], $config); + return call_user_func($this->extensions[$driver], $config, $name); } return $this->factory->make($config, $name); @@ -142,23 +192,34 @@ protected function prepare(Connection $connection) $connection->setEventDispatcher($this->app['events']); } - // The database connection can also utilize a cache manager instance when cache - // functionality is used on queries, which provides an expressive interface - // to caching both fluent queries and Eloquent queries that are executed. - $app = $this->app; - - $connection->setCacheManager(function() use ($app) + // Here we'll set a reconnector callback. This reconnector can be any callable + // so we will set a Closure to reconnect from this manager with the name of + // the connection, which will allow us to reconnect from the connections. + $connection->setReconnector(function($connection) { - return $app['cache']; + $this->reconnect($connection->getName()); }); - // We will setup a Closure to resolve the paginator instance on the connection - // since the Paginator isn't sued on every request and needs quite a few of - // our dependencies. It'll be more efficient to lazily resolve instances. - $connection->setPaginator(function() use ($app) + return $connection; + } + + /** + * Prepare the read write mode for database connection instance. + * + * @param \Illuminate\Database\Connection $connection + * @param string $type + * @return \Illuminate\Database\Connection + */ + protected function setPdoForType(Connection $connection, $type = null) + { + if ($type == 'read') { - return $app['paginator']; - }); + $connection->setPdo($connection->getReadPdo()); + } + elseif ($type == 'write') + { + $connection->setReadPdo($connection->getPdo()); + } return $connection; } @@ -182,7 +243,7 @@ protected function getConfig($name) if (is_null($config = array_get($connections, $name))) { - throw new \InvalidArgumentException("Database [$name] not configured."); + throw new InvalidArgumentException("Database [$name] not configured."); } return $config; @@ -216,7 +277,7 @@ public function setDefaultConnection($name) * @param callable $resolver * @return void */ - public function extend($name, $resolver) + public function extend($name, callable $resolver) { $this->extensions[$name] = $resolver; } diff --git a/DatabaseServiceProvider.php b/DatabaseServiceProvider.php index e91f5a5ad..a4b6eee22 100755 --- a/DatabaseServiceProvider.php +++ b/DatabaseServiceProvider.php @@ -1,45 +1,60 @@ -app['db']); - - Model::setEventDispatcher($this->app['events']); - } - - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - // The connection factory is used to create the actual connection instances on - // the database. We will inject the factory into the manager so that it may - // make the connections while they are actually needed and not of before. - $this->app->bindShared('db.factory', function($app) - { - return new ConnectionFactory($app); - }); - - // The database manager is used to resolve various connections, since multiple - // connections might be managed. It also implements the connection resolver - // interface which may be used by other components requiring connections. - $this->app->bindShared('db', function($app) - { - return new DatabaseManager($app, $app['db.factory']); - }); - } - -} \ No newline at end of file +app['db']); + + Model::setEventDispatcher($this->app['events']); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->registerQueueableEntityResolver(); + + // The connection factory is used to create the actual connection instances on + // the database. We will inject the factory into the manager so that it may + // make the connections while they are actually needed and not of before. + $this->app->singleton('db.factory', function($app) + { + return new ConnectionFactory($app); + }); + + // The database manager is used to resolve various connections, since multiple + // connections might be managed. It also implements the connection resolver + // interface which may be used by other components requiring connections. + $this->app->singleton('db', function($app) + { + return new DatabaseManager($app, $app['db.factory']); + }); + } + + /** + * Register the queueable entity resolver implementation. + * + * @return void + */ + protected function registerQueueableEntityResolver() + { + $this->app->singleton('Illuminate\Contracts\Queue\EntityResolver', function() + { + return new Eloquent\QueueEntityResolver; + }); + } + +} diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index b4ff6415a..2747e2332 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -1,7 +1,9 @@ findMany($id, $columns); + return $this->findMany($id, $columns); } - $this->query->where($this->model->getKeyName(), '=', $id); + $this->query->where($this->model->getQualifiedKeyName(), '=', $id); return $this->first($columns); } @@ -77,25 +93,36 @@ public function find($id, $columns = array('*')) */ public function findMany($id, $columns = array('*')) { - $this->query->whereIn($this->model->getKeyName(), $id); + if (empty($id)) return $this->model->newCollection(); + + $this->query->whereIn($this->model->getQualifiedKeyName(), $id); return $this->get($columns); - } + } /** * Find a model by its primary key or throw an exception. * * @param mixed $id * @param array $columns - * @return \Illuminate\Database\Eloquent\Model|static + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection * - * @throws ModelNotFoundException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function findOrFail($id, $columns = array('*')) { - if ( ! is_null($model = $this->find($id, $columns))) return $model; + $result = $this->find($id, $columns); - throw new ModelNotFoundException; + if (is_array($id)) + { + if (count($result) == count(array_unique($id))) return $result; + } + elseif ( ! is_null($result)) + { + return $result; + } + + throw (new ModelNotFoundException)->setModel(get_class($this->model)); } /** @@ -115,13 +142,13 @@ public function first($columns = array('*')) * @param array $columns * @return \Illuminate\Database\Eloquent\Model|static * - * @throws ModelNotFoundException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function firstOrFail($columns = array('*')) { if ( ! is_null($model = $this->first($columns))) return $model; - throw new ModelNotFoundException; + throw (new ModelNotFoundException)->setModel(get_class($this->model)); } /** @@ -165,7 +192,7 @@ public function pluck($column) * @param callable $callback * @return void */ - public function chunk($count, $callback) + public function chunk($count, callable $callback) { $results = $this->forPage($page = 1, $count)->get(); @@ -210,63 +237,44 @@ public function lists($column, $key = null) } /** - * Get a paginator for the "select" statement. + * Paginate the given query. * - * @param int $perPage + * @param int $perPage * @param array $columns - * @return \Illuminate\Pagination\Paginator + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - public function paginate($perPage = null, $columns = array('*')) + public function paginate($perPage = 15, $columns = ['*']) { - $perPage = $perPage ?: $this->model->getPerPage(); - - $paginator = $this->query->getConnection()->getPaginator(); - - if (isset($this->query->groups)) - { - return $this->groupedPaginate($paginator, $perPage, $columns); - } - else - { - return $this->ungroupedPaginate($paginator, $perPage, $columns); - } - } + $total = $this->query->getCountForPagination(); - /** - * Get a paginator for a grouped statement. - * - * @param \Illuminate\Pagination\Environment $paginator - * @param int $perPage - * @param array $columns - * @return \Illuminate\Pagination\Paginator - */ - protected function groupedPaginate($paginator, $perPage, $columns) - { - $results = $this->get($columns)->all(); + $this->query->forPage( + $page = Paginator::resolveCurrentPage(), + $perPage = $perPage ?: $this->model->getPerPage() + ); - return $this->query->buildRawPaginator($paginator, $results, $perPage); + return new LengthAwarePaginator($this->get($columns)->all(), $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath() + ]); } /** - * Get a paginator for an ungrouped statement. + * Paginate the given query into a simple paginator. * - * @param \Illuminate\Pagination\Environment $paginator - * @param int $perPage + * @param int $perPage * @param array $columns - * @return \Illuminate\Pagination\Paginator + * @return \Illuminate\Contracts\Pagination\Paginator */ - protected function ungroupedPaginate($paginator, $perPage, $columns) + public function simplePaginate($perPage = null, $columns = ['*']) { - $total = $this->query->getPaginationCount(); + $page = Paginator::resolveCurrentPage(); - // Once we have the paginator we need to set the limit and offset values for - // the query so we can get the properly paginated items. Once we have an - // array of items we can create the paginator instances for the items. - $page = $paginator->getCurrentPage($total); + $perPage = $perPage ?: $this->model->getPerPage(); - $this->query->forPage($page, $perPage); + $this->skip(($page - 1) * $perPage)->take($perPage + 1); - return $paginator->make($this->get($columns)->all(), $total, $perPage); + return new Paginator($this->get($columns)->all(), $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath() + ]); } /** @@ -328,36 +336,22 @@ protected function addUpdatedAtColumn(array $values) /** * Delete a record from the database. * - * @return int + * @return mixed */ public function delete() { - if ($this->model->isSoftDeleting()) + if (isset($this->onDelete)) { - return $this->softDelete(); + return call_user_func($this->onDelete, $this); } - else - { - return $this->query->delete(); - } - } - - /** - * Soft delete the record in the database. - * - * @return int - */ - protected function softDelete() - { - $column = $this->model->getDeletedAtColumn(); - return $this->update(array($column => $this->model->freshTimestampString())); + return $this->query->delete(); } /** - * Force a delete on a set of soft deleted models. + * Run the default delete function on the builder. * - * @return int + * @return mixed */ public function forceDelete() { @@ -365,76 +359,21 @@ public function forceDelete() } /** - * Restore the soft-deleted model instances. - * - * @return int - */ - public function restore() - { - if ($this->model->isSoftDeleting()) - { - $column = $this->model->getDeletedAtColumn(); - - return $this->update(array($column => null)); - } - } - - /** - * Include the soft deleted models in the results. - * - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public function withTrashed() - { - $column = $this->model->getQualifiedDeletedAtColumn(); - - foreach ((array) $this->query->wheres as $key => $where) - { - // If the where clause is a soft delete date constraint, we will remove it from - // the query and reset the keys on the wheres. This allows this developer to - // include deleted model in a relationship result set that is lazy loaded. - if ($this->isSoftDeleteConstraint($where, $column)) - { - unset($this->query->wheres[$key]); - - $this->query->wheres = array_values($this->query->wheres); - } - } - - return $this; - } - - /** - * Force the result set to only included soft deletes. - * - * @return \Illuminate\Database\Eloquent\Builder|static - */ - public function onlyTrashed() - { - $this->withTrashed(); - - $this->query->whereNotNull($this->model->getQualifiedDeletedAtColumn()); - - return $this; - } - - /** - * Determine if the given where clause is a soft delete constraint. + * Register a replacement for the default delete function. * - * @param array $where - * @param string $column - * @return bool + * @param \Closure $callback + * @return void */ - protected function isSoftDeleteConstraint(array $where, $column) + public function onDelete(Closure $callback) { - return $where['column'] == $column && $where['type'] == 'Null'; + $this->onDelete = $callback; } /** * Get the hydrated models without eager loading. * * @param array $columns - * @return array|static[] + * @return \Illuminate\Database\Eloquent\Model[] */ public function getModels($columns = array('*')) { @@ -506,7 +445,7 @@ protected function loadRelation(array $models, $name, Closure $constraints) // Once we have the results, we just match those back up to their parent models // using the relationship instance. Then we just return the finished arrays // of models which have been eagerly hydrated and are readied for return. - $results = $relation->get(); + $results = $relation->getEager(); return $relation->match($models, $results, $name); } @@ -519,14 +458,12 @@ protected function loadRelation(array $models, $name, Closure $constraints) */ public function getRelation($relation) { - $me = $this; - // We want to run a relationship query without any constrains so that we will // not have to remove these where clauses manually which gets really hacky // and is error prone while we remove the developer's own where clauses. - $query = Relation::noConstraints(function() use ($me, $relation) + $query = Relation::noConstraints(function() use ($relation) { - return $me->getModel()->$relation(); + return $this->getModel()->$relation(); }); $nested = $this->nestedRelations($relation); @@ -577,7 +514,47 @@ protected function isNested($name, $relation) { $dots = str_contains($name, '.'); - return $dots && starts_with($name, $relation) && $name != $relation; + return $dots && starts_with($name, $relation.'.'); + } + + /** + * Add a basic where clause to the query. + * + * @param string $column + * @param string $operator + * @param mixed $value + * @param string $boolean + * @return $this + */ + public function where($column, $operator = null, $value = null, $boolean = 'and') + { + if ($column instanceof Closure) + { + $query = $this->model->newQueryWithoutScopes(); + + call_user_func($column, $query); + + $this->query->addNestedWhereQuery($query->getQuery(), $boolean); + } + else + { + call_user_func_array(array($this->query, 'where'), func_get_args()); + } + + return $this; + } + + /** + * Add an "or where" clause to the query. + * + * @param string $column + * @param string $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function orWhere($column, $operator = null, $value = null) + { + return $this->where($column, $operator, $value, 'or'); } /** @@ -587,14 +564,19 @@ protected function isNested($name, $relation) * @param string $operator * @param int $count * @param string $boolean - * @param \Closure $callback + * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static */ - public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', $callback = null) + public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null) { + if (strpos($relation, '.') !== false) + { + return $this->hasNested($relation, $operator, $count, $boolean, $callback); + } + $relation = $this->getHasRelationQuery($relation); - $query = $relation->getRelationCountQuery($relation->getRelated()->newQuery()); + $query = $relation->getRelationCountQuery($relation->getRelated()->newQuery(), $this); if ($callback) call_user_func($callback, $query); @@ -602,12 +584,57 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', $ } /** - * Add a relationship count condition to the query with where clauses. + * Add nested relationship count conditions to the query. * - * @param string $relation - * @param \Closure $callback + * @param string $relations * @param string $operator * @param int $count + * @param string $boolean + * @param \Closure $callback + * @return \Illuminate\Database\Eloquent\Builder|static + */ + protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null) + { + $relations = explode('.', $relations); + + // In order to nest "has", we need to add count relation constraints on the + // callback Closure. We'll do this by simply passing the Closure its own + // reference to itself so it calls itself recursively on each segment. + $closure = function ($q) use (&$closure, &$relations, $operator, $count, $boolean, $callback) + { + if (count($relations) > 1) + { + $q->whereHas(array_shift($relations), $closure); + } + else + { + $q->has(array_shift($relations), $operator, $count, $boolean, $callback); + } + }; + + return $this->whereHas(array_shift($relations), $closure); + } + + /** + * Add a relationship count condition to the query. + * + * @param string $relation + * @param string $boolean + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function doesntHave($relation, $boolean = 'and', Closure $callback = null) + { + return $this->has($relation, '<', 1, $boolean, $callback); + } + + /** + * Add a relationship count condition to the query with where clauses. + * + * @param string $relation + * @param \Closure $callback + * @param string $operator + * @param int $count * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereHas($relation, Closure $callback, $operator = '>=', $count = 1) @@ -615,6 +642,18 @@ public function whereHas($relation, Closure $callback, $operator = '>=', $count return $this->has($relation, $operator, $count, 'and', $callback); } + /** + * Add a relationship count condition to the query with where clauses. + * + * @param string $relation + * @param \Closure|null $callback + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public function whereDoesntHave($relation, Closure $callback = null) + { + return $this->doesntHave($relation, 'and', $callback); + } + /** * Add a relationship count condition to the query with an "or". * @@ -631,10 +670,10 @@ public function orHas($relation, $operator = '>=', $count = 1) /** * Add a relationship count condition to the query with where clauses and an "or". * - * @param string $relation + * @param string $relation * @param \Closure $callback - * @param string $operator - * @param int $count + * @param string $operator + * @param int $count * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1) @@ -656,6 +695,11 @@ protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, { $this->mergeWheresToHas($hasQuery, $relation); + if (is_numeric($count)) + { + $count = new Expression($count); + } + return $this->where(new Expression('('.$hasQuery->toSql().')'), $operator, $count, $boolean); } @@ -673,6 +717,8 @@ protected function mergeWheresToHas(Builder $hasQuery, Relation $relation) // the has query, and then copy the bindings from the "has" query to the main. $relationQuery = $relation->getBaseQuery(); + $hasQuery = $hasQuery->getModel()->removeGlobalScopes($hasQuery); + $hasQuery->mergeWheres( $relationQuery->wheres, $relationQuery->getBindings() ); @@ -688,19 +734,17 @@ protected function mergeWheresToHas(Builder $hasQuery, Relation $relation) */ protected function getHasRelationQuery($relation) { - $me = $this; - - return Relation::noConstraints(function() use ($me, $relation) + return Relation::noConstraints(function() use ($relation) { - return $me->getModel()->$relation(); + return $this->getModel()->$relation(); }); } /** * Set the relationships that should be eager loaded. * - * @param dynamic $relations - * @return \Illuminate\Database\Eloquent\Builder|static + * @param mixed $relations + * @return $this */ public function with($relations) { @@ -766,8 +810,8 @@ protected function parseNested($name, $results) if ( ! isset($results[$last = implode('.', $progress)])) { - $results[$last] = function() {}; - } + $results[$last] = function() {}; + } } return $results; @@ -777,7 +821,7 @@ protected function parseNested($name, $results) * Call the given model scope on the underlying model. * * @param string $scope - * @param array $parameters + * @param array $parameters * @return \Illuminate\Database\Query\Builder */ protected function callScope($scope, $parameters) @@ -801,11 +845,13 @@ public function getQuery() * Set the underlying query builder instance. * * @param \Illuminate\Database\Query\Builder $query - * @return void + * @return $this */ public function setQuery($query) { $this->query = $query; + + return $this; } /** @@ -822,11 +868,13 @@ public function getEagerLoads() * Set the relationships being eagerly loaded. * * @param array $eagerLoad - * @return void + * @return $this */ public function setEagerLoads(array $eagerLoad) { $this->eagerLoad = $eagerLoad; + + return $this; } /** @@ -843,7 +891,7 @@ public function getModel() * Set a model instance for the model being queried. * * @param \Illuminate\Database\Eloquent\Model $model - * @return \Illuminate\Database\Eloquent\Builder + * @return $this */ public function setModel(Model $model) { @@ -854,6 +902,29 @@ public function setModel(Model $model) return $this; } + /** + * Extend the builder with a given callback. + * + * @param string $name + * @param \Closure $callback + * @return void + */ + public function macro($name, Closure $callback) + { + $this->macros[$name] = $callback; + } + + /** + * Get the given macro by name. + * + * @param string $name + * @return \Closure + */ + public function getMacro($name) + { + return array_get($this->macros, $name); + } + /** * Dynamically handle calls into the query instance. * @@ -863,15 +934,19 @@ public function setModel(Model $model) */ public function __call($method, $parameters) { - if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) + if (isset($this->macros[$method])) { - return $this->callScope($scope, $parameters); + array_unshift($parameters, $this); + + return call_user_func_array($this->macros[$method], $parameters); } - else + elseif (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { - $result = call_user_func_array(array($this->query, $method), $parameters); + return $this->callScope($scope, $parameters); } + $result = call_user_func_array(array($this->query, $method), $parameters); + return in_array($method, $this->passthru) ? $result : $this; } diff --git a/Eloquent/Collection.php b/Eloquent/Collection.php index 7c83ebb0e..ae661431d 100755 --- a/Eloquent/Collection.php +++ b/Eloquent/Collection.php @@ -28,8 +28,8 @@ public function find($key, $default = null) /** * Load a set of relationships onto the collection. * - * @param dynamic $relations - * @return \Illuminate\Database\Eloquent\Collection + * @param mixed $relations + * @return $this */ public function load($relations) { @@ -49,7 +49,7 @@ public function load($relations) * Add an item to the collection. * * @param mixed $item - * @return \Illuminate\Database\Eloquent\Collection + * @return $this */ public function add($item) { @@ -62,18 +62,24 @@ public function add($item) * Determine if a key exists in the collection. * * @param mixed $key + * @param mixed $value * @return bool */ - public function contains($key) + public function contains($key, $value = null) { - return ! is_null($this->find($key)); + if (func_num_args() == 1) + { + return ! is_null($this->find($key)); + } + + return parent::contains($key, $value); } /** * Fetch a nested element of the collection. * * @param string $key - * @return \Illuminate\Support\Collection + * @return static */ public function fetch($key) { @@ -121,14 +127,14 @@ public function modelKeys() /** * Merge the collection with the given items. * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return \Illuminate\Support\Collection + * @param \ArrayAccess|array $items + * @return static */ - public function merge($collection) + public function merge($items) { - $dictionary = $this->getDictionary($this); + $dictionary = $this->getDictionary(); - foreach ($collection as $item) + foreach ($items as $item) { $dictionary[$item->getKey()] = $item; } @@ -139,14 +145,14 @@ public function merge($collection) /** * Diff the collection with the given items. * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return \Illuminate\Support\Collection + * @param \ArrayAccess|array $items + * @return static */ - public function diff($collection) + public function diff($items) { $diff = new static; - $dictionary = $this->getDictionary($collection); + $dictionary = $this->getDictionary($items); foreach ($this->items as $item) { @@ -162,14 +168,14 @@ public function diff($collection) /** * Intersect the collection with the given items. * - * @param \Illuminate\Support\Collection|\Illuminate\Support\Contracts\ArrayableInterface|array $items - * @return \Illuminate\Support\Collection + * @param \ArrayAccess|array $items + * @return static */ - public function intersect($collection) + public function intersect($items) { $intersect = new static; - $dictionary = $this->getDictionary($collection); + $dictionary = $this->getDictionary($items); foreach ($this->items as $item) { @@ -185,26 +191,54 @@ public function intersect($collection) /** * Return only unique items from the collection. * - * @return \Illuminate\Support\Collection + * @return static */ public function unique() { - $dictionary = $this->getDictionary($this); + $dictionary = $this->getDictionary(); return new static(array_values($dictionary)); } - /* + /** + * Returns only the models from the collection with the specified keys. + * + * @param mixed $keys + * @return static + */ + public function only($keys) + { + $dictionary = array_only($this->getDictionary(), $keys); + + return new static(array_values($dictionary)); + } + + /** + * Returns all models in the collection except the models with specified keys. + * + * @param mixed $keys + * @return static + */ + public function except($keys) + { + $dictionary = array_except($this->getDictionary(), $keys); + + return new static(array_values($dictionary)); + } + + /** * Get a dictionary keyed by primary keys. * - * @param \Illuminate\Support\Collection $collection + * @param \ArrayAccess|array $items * @return array */ - protected function getDictionary($collection) + public function getDictionary($items = null) { + $items = is_null($items) ? $this->items : $items; + $dictionary = array(); - foreach ($collection as $value) + foreach ($items as $value) { $dictionary[$value->getKey()] = $value; } @@ -212,4 +246,14 @@ protected function getDictionary($collection) return $dictionary; } + /** + * Get a base Support collection instance from this collection. + * + * @return \Illuminate\Support\Collection + */ + public function toBase() + { + return new BaseCollection($this->items); + } + } diff --git a/Eloquent/MassAssignmentException.php b/Eloquent/MassAssignmentException.php index 9352aed3c..8874c7c5c 100755 --- a/Eloquent/MassAssignmentException.php +++ b/Eloquent/MassAssignmentException.php @@ -1,3 +1,5 @@ bootIfNotBooted(); + + $this->syncOriginal(); + + $this->fill($attributes); + } /** - * Create a new Eloquent model instance. + * Check if the model needs to be booted and if so, do it. * - * @param array $attributes * @return void */ - public function __construct(array $attributes = array()) + protected function bootIfNotBooted() { - if ( ! isset(static::$booted[get_class($this)])) + $class = get_class($this); + + if ( ! isset(static::$booted[$class])) { - static::$booted[get_class($this)] = true; + static::$booted[$class] = true; $this->fireModelEvent('booting', false); @@ -251,10 +278,6 @@ public function __construct(array $attributes = array()) $this->fireModelEvent('booted', false); } - - $this->syncOriginal(); - - $this->fill($attributes); } /** @@ -280,6 +303,70 @@ protected static function boot() static::$mutatorCache[$class][] = lcfirst($matches[1]); } } + + static::bootTraits(); + } + + /** + * Boot all of the bootable traits on the model. + * + * @return void + */ + protected static function bootTraits() + { + foreach (class_uses_recursive(get_called_class()) as $trait) + { + if (method_exists(get_called_class(), $method = 'boot'.class_basename($trait))) + { + forward_static_call([get_called_class(), $method]); + } + } + } + + /** + * Register a new global scope on the model. + * + * @param \Illuminate\Database\Eloquent\ScopeInterface $scope + * @return void + */ + public static function addGlobalScope(ScopeInterface $scope) + { + static::$globalScopes[get_called_class()][get_class($scope)] = $scope; + } + + /** + * Determine if a model has a global scope. + * + * @param \Illuminate\Database\Eloquent\ScopeInterface $scope + * @return bool + */ + public static function hasGlobalScope($scope) + { + return ! is_null(static::getGlobalScope($scope)); + } + + /** + * Get a global scope registered with the model. + * + * @param \Illuminate\Database\Eloquent\ScopeInterface $scope + * @return \Illuminate\Database\Eloquent\ScopeInterface|null + */ + public static function getGlobalScope($scope) + { + return array_first(static::$globalScopes[get_called_class()], function($key, $value) use ($scope) + { + return $scope instanceof $value; + }); + } + + /** + * Get the global scopes for this class instance. + * + * @return \Illuminate\Database\Eloquent\ScopeInterface[] + */ + public function getGlobalScopes() + { + return array_get(static::$globalScopes, get_class($this), []); } /** @@ -310,12 +397,14 @@ public static function observe($class) * Fill the model with an array of attributes. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model|static + * @return $this * - * @throws MassAssignmentException + * @throws \Illuminate\Database\Eloquent\MassAssignmentException */ public function fill(array $attributes) { + $totallyGuarded = $this->totallyGuarded(); + foreach ($this->fillableFromArray($attributes) as $key => $value) { $key = $this->removeTableFromKey($key); @@ -327,7 +416,7 @@ public function fill(array $attributes) { $this->setAttribute($key, $value); } - elseif ($this->totallyGuarded()) + elseif ($totallyGuarded) { throw new MassAssignmentException($key); } @@ -336,6 +425,23 @@ public function fill(array $attributes) return $this; } + /** + * Fill the model with an array of attributes. Force mass assignment. + * + * @param array $attributes + * @return $this + */ + public function forceFill(array $attributes) + { + static::unguard(); + + $this->fill($attributes); + + static::reguard(); + + return $this; + } + /** * Get the fillable attributes of a given array. * @@ -357,7 +463,7 @@ protected function fillableFromArray(array $attributes) * * @param array $attributes * @param bool $exists - * @return \Illuminate\Database\Eloquent\Model|static + * @return static */ public function newInstance($attributes = array(), $exists = false) { @@ -375,7 +481,7 @@ public function newInstance($attributes = array(), $exists = false) * Create a new model instance that is existing. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model|static + * @return static */ public function newFromBuilder($attributes = array()) { @@ -386,11 +492,59 @@ public function newFromBuilder($attributes = array()) return $instance; } + /** + * Create a collection of models from plain arrays. + * + * @param array $items + * @param string $connection + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function hydrate(array $items, $connection = null) + { + $collection = with($instance = new static)->newCollection(); + + foreach ($items as $item) + { + $model = $instance->newFromBuilder($item); + + if ( ! is_null($connection)) + { + $model->setConnection($connection); + } + + $collection->push($model); + } + + return $collection; + } + + /** + * Create a collection of models from a raw query. + * + * @param string $query + * @param array $bindings + * @param string $connection + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function hydrateRaw($query, $bindings = array(), $connection = null) + { + $instance = new static; + + if ( ! is_null($connection)) + { + $instance->setConnection($connection); + } + + $items = $instance->getConnection()->select($query, $bindings); + + return static::hydrate($items, $connection); + } + /** * Save a new model and return the instance. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model|static + * @return static */ public static function create(array $attributes) { @@ -401,15 +555,32 @@ public static function create(array $attributes) return $model; } + /** + * Save a new model and return the instance. Allow mass-assignment. + * + * @param array $attributes + * @return static + */ + public static function forceCreate(array $attributes) + { + static::unguard(); + + $model = static::create($attributes); + + static::reguard(); + + return $model; + } + /** * Get the first record matching the attributes or create it. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model + * @return static */ public static function firstOrCreate(array $attributes) { - if ( ! is_null($instance = static::firstByAttributes($attributes))) + if ( ! is_null($instance = static::where($attributes)->first())) { return $instance; } @@ -421,11 +592,11 @@ public static function firstOrCreate(array $attributes) * Get the first record matching the attributes or instantiate it. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model + * @return static */ public static function firstOrNew(array $attributes) { - if ( ! is_null($instance = static::firstByAttributes($attributes))) + if ( ! is_null($instance = static::where($attributes)->first())) { return $instance; } @@ -434,38 +605,47 @@ public static function firstOrNew(array $attributes) } /** - * Get the first model for the given attributes. + * Create or update a record matching the attributes, and fill it with values. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model|null + * @param array $values + * @return static */ - protected static function firstByAttributes($attributes) + public static function updateOrCreate(array $attributes, array $values = array()) { - $query = static::query(); + $instance = static::firstOrNew($attributes); - foreach ($attributes as $key => $value) - { - $query->where($key, $value); - } + $instance->fill($values)->save(); - return $query->first() ?: null; + return $instance; + } + + /** + * Get the first model for the given attributes. + * + * @param array $attributes + * @return static|null + */ + protected static function firstByAttributes($attributes) + { + return static::where($attributes)->first(); } /** * Begin querying the model. * - * @return \Illuminate\Database\Eloquent\Builder|static + * @return \Illuminate\Database\Eloquent\Builder */ public static function query() { - return with(new static)->newQuery(); + return (new static)->newQuery(); } /** * Begin querying the model on a given connection. * * @param string $connection - * @return \Illuminate\Database\Eloquent\Builder|static + * @return \Illuminate\Database\Eloquent\Builder */ public static function on($connection = null) { @@ -479,6 +659,18 @@ public static function on($connection = null) return $instance->newQuery(); } + /** + * Begin querying the model on the write connection. + * + * @return \Illuminate\Database\Query\Builder + */ + public static function onWriteConnection() + { + $instance = new static; + + return $instance->newQuery()->useWritePdo(); + } + /** * Get all of the models from the database. * @@ -497,36 +689,49 @@ public static function all($columns = array('*')) * * @param mixed $id * @param array $columns - * @return \Illuminate\Database\Eloquent\Model|Collection|static + * @return \Illuminate\Support\Collection|static|null */ public static function find($id, $columns = array('*')) { $instance = new static; + if (is_array($id) && empty($id)) return $instance->newCollection(); + return $instance->newQuery()->find($id, $columns); } /** - * Find a model by its primary key or throw an exception. + * Find a model by its primary key or return new static. * * @param mixed $id * @param array $columns - * @return \Illuminate\Database\Eloquent\Model|Collection|static - * - * @throws ModelNotFoundException + * @return \Illuminate\Support\Collection|static */ - public static function findOrFail($id, $columns = array('*')) + public static function findOrNew($id, $columns = array('*')) { if ( ! is_null($model = static::find($id, $columns))) return $model; - throw new ModelNotFoundException(get_called_class().' model not found'); + return new static; + } + + /** + * Reload a fresh model instance from the database. + * + * @param array $with + * @return $this + */ + public function fresh(array $with = array()) + { + $key = $this->getKeyName(); + + return $this->exists ? static::with($with)->where($key, $this->getKey())->first() : null; } /** * Eager load relations on the model. * * @param array|string $relations - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function load($relations) { @@ -580,6 +785,7 @@ public function hasOne($related, $foreignKey = null, $localKey = null) * @param string $name * @param string $type * @param string $id + * @param string $localKey * @return \Illuminate\Database\Eloquent\Relations\MorphOne */ public function morphOne($related, $name, $type = null, $id = null, $localKey = null) @@ -608,10 +814,10 @@ public function belongsTo($related, $foreignKey = null, $otherKey = null, $relat { // If no relation name was given, we will use this debug backtrace to extract // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relatinoships. + // of the time this will be what we desire to use for the relationships. if (is_null($relation)) { - list(, $caller) = debug_backtrace(false); + list(, $caller) = debug_backtrace(false, 2); $relation = $caller['function']; } @@ -637,12 +843,12 @@ public function belongsTo($related, $foreignKey = null, $otherKey = null, $relat } /** - * Define an polymorphic, inverse one-to-one or many relationship. + * Define a polymorphic, inverse one-to-one or many relationship. * * @param string $name * @param string $type * @param string $id - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ public function morphTo($name = null, $type = null, $id = null) { @@ -651,19 +857,34 @@ public function morphTo($name = null, $type = null, $id = null) // use that to get both the class and foreign key that will be utilized. if (is_null($name)) { - list(, $caller) = debug_backtrace(false); + list(, $caller) = debug_backtrace(false, 2); $name = snake_case($caller['function']); } - // Next we will guess the type and ID if necessary. The type and IDs may also - // be passed into the function so that the developers may manually specify - // them on the relations. Otherwise, we will just make a great estimate. list($type, $id) = $this->getMorphs($name, $type, $id); - $class = $this->$type; + // If the type value is null it is probably safe to assume we're eager loading + // the relationship. When that is the case we will pass in a dummy query as + // there are multiple types in the morph and we can't use single queries. + if (is_null($class = $this->$type)) + { + return new MorphTo( + $this->newQuery(), $this, $id, null, $type, $name + ); + } + + // If we are not eager loading the relationship we will essentially treat this + // as a belongs-to style relationship since morph-to extends that class and + // we will pass in the appropriate values so that it behaves as expected. + else + { + $instance = new $class; - return $this->belongsTo($class, $id); + return new MorphTo( + $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name + ); + } } /** @@ -702,7 +923,7 @@ public function hasManyThrough($related, $through, $firstKey = null, $secondKey $secondKey = $secondKey ?: $through->getForeignKey(); - return new HasManyThrough(with(new $related)->newQuery(), $this, $through, $firstKey, $secondKey); + return new HasManyThrough((new $related)->newQuery(), $this, $through, $firstKey, $secondKey); } /** @@ -748,9 +969,7 @@ public function belongsToMany($related, $table = null, $foreignKey = null, $othe // title of this relation since that is a great convention to apply. if (is_null($relation)) { - $caller = $this->getBelongsToManyCaller(); - - $name = $caller['function']; + $relation = $this->getBelongsToManyCaller(); } // First, we'll need to determine the foreign key and "other key" for the @@ -779,14 +998,14 @@ public function belongsToMany($related, $table = null, $foreignKey = null, $othe } /** - * Define a many-to-many relationship. + * Define a polymorphic many-to-many relationship. * * @param string $related * @param string $name * @param string $table * @param string $foreignKey * @param string $otherKey - * @param bool $inverse + * @param bool $inverse * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ public function morphToMany($related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false) @@ -811,19 +1030,18 @@ public function morphToMany($related, $name, $table = null, $foreignKey = null, return new MorphToMany( $query, $this, $name, $table, $foreignKey, - $otherKey, $caller['function'], $inverse + $otherKey, $caller, $inverse ); } /** - * Define a many-to-many relationship. + * Define a polymorphic, inverse many-to-many relationship. * * @param string $related * @param string $name * @param string $table * @param string $foreignKey * @param string $otherKey - * @param string $morphClass * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ public function morphedByMany($related, $name, $table = null, $foreignKey = null, $otherKey = null) @@ -841,18 +1059,20 @@ public function morphedByMany($related, $name, $table = null, $foreignKey = null /** * Get the relationship name of the belongs to many. * - * @return string + * @return string */ protected function getBelongsToManyCaller() { $self = __FUNCTION__; - return array_first(debug_backtrace(false), function($trace) use ($self) + $caller = array_first(debug_backtrace(false), function($key, $trace) use ($self) { $caller = $trace['function']; return ( ! in_array($caller, Model::$manyMethods) && $caller != $self); }); + + return ! is_null($caller) ? $caller['function'] : null; } /** @@ -884,10 +1104,15 @@ public function joiningTable($related) * Destroy the models for the given IDs. * * @param array|int $ids - * @return void + * @return int */ public static function destroy($ids) { + // We'll initialize a count here so we will return the total number of deletes + // for the operation. The developers can then check this number as a boolean + // type value or get this total count of records deleted for logging, etc. + $count = 0; + $ids = is_array($ids) ? $ids : func_get_args(); $instance = new static; @@ -899,17 +1124,25 @@ public static function destroy($ids) foreach ($instance->whereIn($key, $ids)->get() as $model) { - $model->delete(); + if ($model->delete()) $count++; } + + return $count; } /** * Delete the model from the database. * * @return bool|null + * @throws \Exception */ public function delete() { + if (is_null($this->primaryKey)) + { + throw new Exception("No primary key defined on model."); + } + if ($this->exists) { if ($this->fireModelEvent('deleting') === false) return false; @@ -935,20 +1168,13 @@ public function delete() /** * Force a hard delete on a soft deleted model. * + * This method protects developers from running forceDelete when trait is missing. + * * @return void */ public function forceDelete() { - $softDelete = $this->softDelete; - - // We will temporarily disable false delete to allow us to perform the real - // delete operation against the model. We will then restore the deleting - // state to what this was prior to this given hard deleting operation. - $this->softDelete = false; - - $this->delete(); - - $this->softDelete = $softDelete; + return $this->delete(); } /** @@ -958,158 +1184,103 @@ public function forceDelete() */ protected function performDeleteOnModel() { - $query = $this->newQuery()->where($this->getKeyName(), $this->getKey()); - - if ($this->softDelete) - { - $this->{static::DELETED_AT} = $time = $this->freshTimestamp(); - - $query->update(array(static::DELETED_AT => $this->fromDateTime($time))); - } - else - { - $query->delete(); - } - } - - /** - * Restore a soft-deleted model instance. - * - * @return bool|null - */ - public function restore() - { - if ($this->softDelete) - { - // If the restoring event does not return false, we will proceed with this - // restore operation. Otherwise, we bail out so the developer will stop - // the restore totally. We will clear the deleted timestamp and save. - if ($this->fireModelEvent('restoring') === false) - { - return false; - } - - $this->{static::DELETED_AT} = null; - - // Once we have saved the model, we will fire the "restored" event so this - // developer will do anything they need to after a restore operation is - // totally finished. Then we will return the result of the save call. - $result = $this->save(); - - $this->fireModelEvent('restored', false); - - return $result; - } + $this->newQuery()->where($this->getKeyName(), $this->getKey())->delete(); } /** * Register a saving model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function saving($callback) + public static function saving($callback, $priority = 0) { - static::registerModelEvent('saving', $callback); + static::registerModelEvent('saving', $callback, $priority); } /** * Register a saved model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function saved($callback) + public static function saved($callback, $priority = 0) { - static::registerModelEvent('saved', $callback); + static::registerModelEvent('saved', $callback, $priority); } /** * Register an updating model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function updating($callback) + public static function updating($callback, $priority = 0) { - static::registerModelEvent('updating', $callback); + static::registerModelEvent('updating', $callback, $priority); } /** * Register an updated model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function updated($callback) + public static function updated($callback, $priority = 0) { - static::registerModelEvent('updated', $callback); + static::registerModelEvent('updated', $callback, $priority); } /** * Register a creating model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function creating($callback) + public static function creating($callback, $priority = 0) { - static::registerModelEvent('creating', $callback); + static::registerModelEvent('creating', $callback, $priority); } /** * Register a created model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function created($callback) + public static function created($callback, $priority = 0) { - static::registerModelEvent('created', $callback); + static::registerModelEvent('created', $callback, $priority); } /** * Register a deleting model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function deleting($callback) + public static function deleting($callback, $priority = 0) { - static::registerModelEvent('deleting', $callback); + static::registerModelEvent('deleting', $callback, $priority); } /** * Register a deleted model event with the dispatcher. * * @param \Closure|string $callback + * @param int $priority * @return void */ - public static function deleted($callback) + public static function deleted($callback, $priority = 0) { - static::registerModelEvent('deleted', $callback); - } - - /** - * Register a restoring model event with the dispatcher. - * - * @param \Closure|string $callback - * @return void - */ - public static function restoring($callback) - { - static::registerModelEvent('restoring', $callback); - } - - /** - * Register a restored model event with the dispatcher. - * - * @param \Closure|string $callback - * @return void - */ - public static function restored($callback) - { - static::registerModelEvent('restored', $callback); + static::registerModelEvent('deleted', $callback, $priority); } /** @@ -1134,15 +1305,16 @@ public static function flushEventListeners() * * @param string $event * @param \Closure|string $callback + * @param int $priority * @return void */ - protected static function registerModelEvent($event, $callback) + protected static function registerModelEvent($event, $callback, $priority = 0) { if (isset(static::$dispatcher)) { $name = get_called_class(); - static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback); + static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback, $priority); } } @@ -1163,6 +1335,43 @@ public function getObservableEvents() ); } + /** + * Set the observable event names. + * + * @param array $observables + * @return void + */ + public function setObservableEvents(array $observables) + { + $this->observables = $observables; + } + + /** + * Add an observable event name. + * + * @param mixed $observables + * @return void + */ + public function addObservableEvents($observables) + { + $observables = is_array($observables) ? $observables : func_get_args(); + + $this->observables = array_unique(array_merge($this->observables, $observables)); + } + + /** + * Remove an observable event name. + * + * @param mixed $observables + * @return void + */ + public function removeObservableEvents($observables) + { + $observables = is_array($observables) ? $observables : func_get_args(); + + $this->observables = array_diff($this->observables, $observables); + } + /** * Increment a column's value by a given amount. * @@ -1204,14 +1413,31 @@ protected function incrementOrDecrement($column, $amount, $method) return $query->{$method}($column, $amount); } + $this->incrementOrDecrementAttributeValue($column, $amount, $method); + return $query->where($this->getKeyName(), $this->getKey())->{$method}($column, $amount); } + /** + * Increment the underlying attribute value and sync with original. + * + * @param string $column + * @param int $amount + * @param string $method + * @return void + */ + protected function incrementOrDecrementAttributeValue($column, $amount, $method) + { + $this->{$column} = $this->{$column} + ($method == 'increment' ? $amount : $amount * -1); + + $this->syncOriginalAttribute($column); + } + /** * Update the model in the database. * * @param array $attributes - * @return mixed + * @return bool|int */ public function update(array $attributes = array()) { @@ -1237,7 +1463,10 @@ public function push() // us to recurse into all of these nested relations for the model instance. foreach ($this->relations as $models) { - foreach (Collection::make($models) as $model) + $models = $models instanceof Collection + ? $models->all() : array($models); + + foreach (array_filter($models) as $model) { if ( ! $model->push()) return false; } @@ -1254,10 +1483,10 @@ public function push() */ public function save(array $options = array()) { - $query = $this->newQueryWithDeleted(); + $query = $this->newQueryWithoutScopes(); // If the "saving" event returns false we'll bail out of the save and return - // false, indicating that the save failed. This gives an opportunities to + // false, indicating that the save failed. This provides a chance for any // listeners to cancel save operations if validations fail or whatever. if ($this->fireModelEvent('saving') === false) { @@ -1269,7 +1498,7 @@ public function save(array $options = array()) // clause to only update this model. Otherwise, we'll just insert them. if ($this->exists) { - $saved = $this->performUpdate($query); + $saved = $this->performUpdate($query, $options); } // If the model is brand new, we'll insert it into our database and set the @@ -1277,7 +1506,7 @@ public function save(array $options = array()) // which is typically an auto-increment value managed by the database. else { - $saved = $this->performInsert($query); + $saved = $this->performInsert($query, $options); } if ($saved) $this->finishSave($options); @@ -1293,10 +1522,10 @@ public function save(array $options = array()) */ protected function finishSave(array $options) { - $this->syncOriginal(); - $this->fireModelEvent('saved', false); + $this->syncOriginal(); + if (array_get($options, 'touch', true)) $this->touchOwners(); } @@ -1304,9 +1533,10 @@ protected function finishSave(array $options) * Perform a model update operation. * * @param \Illuminate\Database\Eloquent\Builder $query - * @return bool + * @param array $options + * @return bool|null */ - protected function performUpdate(Builder $query) + protected function performUpdate(Builder $query, array $options = []) { $dirty = $this->getDirty(); @@ -1323,19 +1553,22 @@ protected function performUpdate(Builder $query) // First we need to create a fresh query instance and touch the creation and // update timestamp on the model which are maintained by us for developer // convenience. Then we will just continue saving the model instances. - if ($this->timestamps) + if ($this->timestamps && array_get($options, 'timestamps', true)) { $this->updateTimestamps(); - - $dirty = $this->getDirty(); } // Once we have run the update operation, we will fire the "updated" event for // this model instance. This will allow developers to hook into these after // models are updated, giving them a chance to do any special processing. - $this->setKeysForSaveQuery($query)->update($dirty); + $dirty = $this->getDirty(); + + if (count($dirty) > 0) + { + $this->setKeysForSaveQuery($query)->update($dirty); - $this->fireModelEvent('updated', false); + $this->fireModelEvent('updated', false); + } } return true; @@ -1345,16 +1578,17 @@ protected function performUpdate(Builder $query) * Perform a model insert operation. * * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $options * @return bool */ - protected function performInsert(Builder $query) + protected function performInsert(Builder $query, array $options = []) { if ($this->fireModelEvent('creating') === false) return false; // First we'll need to create a fresh query instance and touch the creation and // update timestamps on this model, which are maintained by us for developer // convenience. After, we will just continue saving these model instances. - if ($this->timestamps) + if ($this->timestamps && array_get($options, 'timestamps', true)) { $this->updateTimestamps(); } @@ -1411,6 +1645,11 @@ public function touchOwners() foreach ($this->touches as $relation) { $this->$relation()->touch(); + + if ( ! is_null($this->$relation)) + { + $this->$relation->touchOwners(); + } } } @@ -1470,10 +1709,8 @@ protected function getKeyForSaveQuery() { return $this->original[$this->getKeyName()]; } - else - { - return $this->getAttribute($this->getKeyName()); - } + + return $this->getAttribute($this->getKeyName()); } /** @@ -1483,6 +1720,8 @@ protected function getKeyForSaveQuery() */ public function touch() { + if ( ! $this->timestamps) return false; + $this->updateTimestamps(); return $this->save(); @@ -1550,26 +1789,6 @@ public function getUpdatedAtColumn() return static::UPDATED_AT; } - /** - * Get the name of the "deleted at" column. - * - * @return string - */ - public function getDeletedAtColumn() - { - return static::DELETED_AT; - } - - /** - * Get the fully qualified "deleted at" column. - * - * @return string - */ - public function getQualifiedDeletedAtColumn() - { - return $this->getTable().'.'.$this->getDeletedAtColumn(); - } - /** * Get a fresh timestamp for the model. * @@ -1593,68 +1812,86 @@ public function freshTimestampString() /** * Get a new query builder for the model's table. * - * @param bool $excludeDeleted - * @return \Illuminate\Database\Eloquent\Builder|static + * @return \Illuminate\Database\Eloquent\Builder */ - public function newQuery($excludeDeleted = true) + public function newQuery() { - $builder = new Builder($this->newBaseQueryBuilder()); + $builder = $this->newQueryWithoutScopes(); - // Once we have the query builders, we will set the model instances so the - // builder can easily access any information it may need from the model - // while it is constructing and executing various queries against it. - $builder->setModel($this)->with($this->with); + return $this->applyGlobalScopes($builder); + } - if ($excludeDeleted && $this->softDelete) - { - $builder->whereNull($this->getQualifiedDeletedAtColumn()); - } + /** + * Get a new query instance without a given scope. + * + * @param \Illuminate\Database\Eloquent\ScopeInterface $scope + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQueryWithoutScope($scope) + { + $this->getGlobalScope($scope)->remove($builder = $this->newQuery(), $this); return $builder; } /** - * Get a new query builder that includes soft deletes. + * Get a new query builder that doesn't have any global scopes. * * @return \Illuminate\Database\Eloquent\Builder|static */ - public function newQueryWithDeleted() + public function newQueryWithoutScopes() { - return $this->newQuery(false); + $builder = $this->newEloquentBuilder( + $this->newBaseQueryBuilder() + ); + + // Once we have the query builders, we will set the model instances so the + // builder can easily access any information it may need from the model + // while it is constructing and executing various queries against it. + return $builder->setModel($this)->with($this->with); } /** - * Determine if the model instance has been soft-deleted. + * Apply all of the global scopes to an Eloquent builder. * - * @return bool + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return \Illuminate\Database\Eloquent\Builder */ - public function trashed() + public function applyGlobalScopes($builder) { - return $this->softDelete && ! is_null($this->{static::DELETED_AT}); + foreach ($this->getGlobalScopes() as $scope) + { + $scope->apply($builder, $this); + } + + return $builder; } /** - * Get a new query builder that includes soft deletes. + * Remove all of the global scopes from an Eloquent builder. * - * @return \Illuminate\Database\Eloquent\Builder|static + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return \Illuminate\Database\Eloquent\Builder */ - public static function withTrashed() + public function removeGlobalScopes($builder) { - return with(new static)->newQueryWithDeleted(); + foreach ($this->getGlobalScopes() as $scope) + { + $scope->remove($builder, $this); + } + + return $builder; } /** - * Get a new query builder that only includes soft deletes. + * Create a new Eloquent query builder for the model. * + * @param \Illuminate\Database\Query\Builder $query * @return \Illuminate\Database\Eloquent\Builder|static */ - public static function onlyTrashed() + public function newEloquentBuilder($query) { - $instance = new static; - - $column = $instance->getQualifiedDeletedAtColumn(); - - return $instance->newQueryWithDeleted()->whereNotNull($column); + return new Builder($query); } /** @@ -1689,7 +1926,7 @@ public function newCollection(array $models = array()) * @param array $attributes * @param string $table * @param bool $exists - * @return \Illuminate\Database\Eloquent\Relation\Pivot + * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newPivot(Model $parent, array $attributes, $table, $exists) { @@ -1729,6 +1966,16 @@ public function getKey() return $this->getAttribute($this->getKeyName()); } + /** + * Get the queueable identity for the entity. + * + * @return mixed + */ + public function getQueueableId() + { + return $this->getKey(); + } + /** * Get the primary key for the model. * @@ -1739,6 +1986,17 @@ public function getKeyName() return $this->primaryKey; } + /** + * Set the primary key for the model. + * + * @param string $key + * @return void + */ + public function setKeyName($key) + { + $this->primaryKey = $key; + } + /** * Get the table qualified key name. * @@ -1750,34 +2008,33 @@ public function getQualifiedKeyName() } /** - * Determine if the model uses timestamps. + * Get the value of the model's route key. * - * @return bool + * @return mixed */ - public function usesTimestamps() + public function getRouteKey() { - return $this->timestamps; + return $this->getAttribute($this->getRouteKeyName()); } /** - * Determine if the model instance uses soft deletes. + * Get the route key for the model. * - * @return bool + * @return string */ - public function isSoftDeleting() + public function getRouteKeyName() { - return $this->softDelete; + return $this->getKeyName(); } /** - * Set the soft deleting property on the model. + * Determine if the model uses timestamps. * - * @param bool $enabled - * @return void + * @return bool */ - public function setSoftDeleting($enabled) + public function usesTimestamps() { - $this->softDelete = $enabled; + return $this->timestamps; } /** @@ -1797,6 +2054,16 @@ protected function getMorphs($name, $type, $id) return array($type, $id); } + /** + * Get the class name for polymorphic relations. + * + * @return string + */ + public function getMorphClass() + { + return $this->morphClass ?: get_class($this); + } + /** * Get the number of models to return per page. * @@ -1808,7 +2075,7 @@ public function getPerPage() } /** - * Set the number of models ot return per page. + * Set the number of models to return per page. * * @param int $perPage * @return void @@ -1849,6 +2116,29 @@ public function setHidden(array $hidden) $this->hidden = $hidden; } + /** + * Add hidden attributes for the model. + * + * @param array|string|null $attributes + * @return void + */ + public function addHidden($attributes = null) + { + $attributes = is_array($attributes) ? $attributes : func_get_args(); + + $this->hidden = array_merge($this->hidden, $attributes); + } + + /** + * Get the visible attributes for the model. + * + * @return array + */ + public function getVisible() + { + return $this->visible; + } + /** * Set the visible attributes for the model. * @@ -1860,6 +2150,19 @@ public function setVisible(array $visible) $this->visible = $visible; } + /** + * Add visible attributes for the model. + * + * @param array|string|null $attributes + * @return void + */ + public function addVisible($attributes = null) + { + $attributes = is_array($attributes) ? $attributes : func_get_args(); + + $this->visible = array_merge($this->visible, $attributes); + } + /** * Set the accessors to append to model arrays. * @@ -1885,7 +2188,7 @@ public function getFillable() * Set the fillable attributes for the model. * * @param array $fillable - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function fillable(array $fillable) { @@ -1894,11 +2197,21 @@ public function fillable(array $fillable) return $this; } + /** + * Get the guarded attributes for the model. + * + * @return array + */ + public function getGuarded() + { + return $this->guarded; + } + /** * Set the guarded attributes for the model. * * @param array $guarded - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function guard(array $guarded) { @@ -2045,6 +2358,16 @@ public function toJson($options = 0) return json_encode($this->toArray(), $options); } + /** + * Convert the object into something JSON serializable. + * + * @return array + */ + public function jsonSerialize() + { + return $this->toArray(); + } + /** * Convert the model instance to an array. * @@ -2066,14 +2389,39 @@ public function attributesToArray() { $attributes = $this->getArrayableAttributes(); + // If an attribute is a date, we will cast it to a string after converting it + // to a DateTime / Carbon instance. This is so we will get some consistent + // formatting while accessing attributes vs. arraying / JSONing a model. + foreach ($this->getDates() as $key) + { + if ( ! isset($attributes[$key])) continue; + + $attributes[$key] = (string) $this->asDateTime($attributes[$key]); + } + + $mutatedAttributes = $this->getMutatedAttributes(); + // We want to spin through all the mutated attributes for this model and call // the mutator for the attribute. We cache off every mutated attributes so // we don't have to constantly check on attributes that actually change. - foreach ($this->getMutatedAttributes() as $key) + foreach ($mutatedAttributes as $key) { if ( ! array_key_exists($key, $attributes)) continue; - $attributes[$key] = $this->mutateAttribute( + $attributes[$key] = $this->mutateAttributeForArray( + $key, $attributes[$key] + ); + } + + // Next we will handle any casts that have been setup for this model and cast + // the values to their appropriate type. If the attribute has a mutator we + // will not perform the cast on those attributes to avoid any confusion. + foreach ($this->casts as $key => $value) + { + if ( ! array_key_exists($key, $attributes) || + in_array($key, $mutatedAttributes)) continue; + + $attributes[$key] = $this->castAttribute( $key, $attributes[$key] ); } @@ -2081,9 +2429,9 @@ public function attributesToArray() // Here we will grab all of the appended, calculated attributes to this model // as these attributes are not really in the attributes array, but are run // when we need to array or JSON the model for convenience to the coder. - foreach ($this->appends as $key) + foreach ($this->getArrayableAppends() as $key) { - $attributes[$key] = $this->mutateAttribute($key, null); + $attributes[$key] = $this->mutateAttributeForArray($key, null); } return $attributes; @@ -2099,6 +2447,20 @@ protected function getArrayableAttributes() return $this->getArrayableItems($this->attributes); } + /** + * Get all of the appendable values that are arrayable. + * + * @return array + */ + protected function getArrayableAppends() + { + if ( ! count($this->appends)) return []; + + return $this->getArrayableItems( + array_combine($this->appends, $this->appends) + ); + } + /** * Get the model's relationships in array form. * @@ -2115,7 +2477,7 @@ public function relationsToArray() // If the values implements the Arrayable interface we can just call this // toArray method on the instances which will convert both models and // collections to their proper array form and we'll set the values. - if ($value instanceof ArrayableInterface) + if ($value instanceof Arrayable) { $relation = $value->toArray(); } @@ -2143,6 +2505,8 @@ public function relationsToArray() { $attributes[$key] = $relation; } + + unset($relation); } return $attributes; @@ -2203,11 +2567,9 @@ public function getAttribute($key) // If the "attribute" exists as a method on the model, we will just assume // it is a relationship and will load and return results from the query // and hydrate the relationship's value on the "relationships" array. - $camelKey = camel_case($key); - - if (method_exists($this, $camelKey)) + if (method_exists($this, $key)) { - return $this->getRelationshipFromMethod($key, $camelKey); + return $this->getRelationshipFromMethod($key); } } @@ -2229,6 +2591,14 @@ protected function getAttributeValue($key) return $this->mutateAttribute($key, $value); } + // If the attribute exists within the cast array, we will convert it to + // an appropriate native PHP type dependant upon the associated value + // given with the key in the pair. Dayle made this comment line up. + if ($this->hasCast($key)) + { + $value = $this->castAttribute($key, $value); + } + // If the attribute is listed as a date, we will convert it to a DateTime // instance on retrieval, which makes it quite convenient to work with // date fields without having to create a mutator for each property. @@ -2257,13 +2627,14 @@ protected function getAttributeFromArray($key) /** * Get a relationship value from a method. * - * @param string $key - * @param string $camelKey + * @param string $method * @return mixed + * + * @throws \LogicException */ - protected function getRelationshipFromMethod($key, $camelKey) + protected function getRelationshipFromMethod($method) { - $relations = $this->$camelKey(); + $relations = $this->$method(); if ( ! $relations instanceof Relation) { @@ -2271,7 +2642,7 @@ protected function getRelationshipFromMethod($key, $camelKey) . 'Illuminate\Database\Eloquent\Relations\Relation'); } - return $this->relations[$key] = $relations->getResults(); + return $this->relations[$method] = $relations->getResults(); } /** @@ -2297,6 +2668,93 @@ protected function mutateAttribute($key, $value) return $this->{'get'.studly_case($key).'Attribute'}($value); } + /** + * Get the value of an attribute using its mutator for array conversion. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function mutateAttributeForArray($key, $value) + { + $value = $this->mutateAttribute($key, $value); + + return $value instanceof Arrayable ? $value->toArray() : $value; + } + + /** + * Determine whether an attribute should be casted to a native type. + * + * @param string $key + * @return bool + */ + protected function hasCast($key) + { + return array_key_exists($key, $this->casts); + } + + /** + * Determine whether a value is JSON castable for inbound manipulation. + * + * @param string $key + * @return bool + */ + protected function isJsonCastable($key) + { + if ($this->hasCast($key)) + { + $type = $this->getCastType($key); + + return $type === 'array' || $type === 'json' || $type === 'object'; + } + + return false; + } + + /** + * Get the type of cast for a model attribute. + * + * @param string $key + * @return string + */ + protected function getCastType($key) + { + return trim(strtolower($this->casts[$key])); + } + + /** + * Cast an attribute to a native PHP type. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function castAttribute($key, $value) + { + switch ($this->getCastType($key)) + { + case 'int': + case 'integer': + return (int) $value; + case 'real': + case 'float': + case 'double': + return (float) $value; + case 'string': + return (string) $value; + case 'bool': + case 'boolean': + return (bool) $value; + case 'object': + return json_decode($value); + case 'array': + case 'json': + return json_decode($value, true); + default: + return $value; + } + } + /** * Set a given attribute on the model. * @@ -2319,12 +2777,14 @@ public function setAttribute($key, $value) // If an attribute is listed as a "date", we'll convert it from a DateTime // instance into a form proper for storage on the database tables using // the connection grammar's date format. We will auto set the values. - elseif (in_array($key, $this->getDates())) + elseif (in_array($key, $this->getDates()) && $value) { - if ($value) - { - $value = $this->fromDateTime($value); - } + $value = $this->fromDateTime($value); + } + + if ($this->isJsonCastable($key)) + { + $value = json_encode($value); } $this->attributes[$key] = $value; @@ -2348,7 +2808,7 @@ public function hasSetMutator($key) */ public function getDates() { - $defaults = array(static::CREATED_AT, static::UPDATED_AT, static::DELETED_AT); + $defaults = array(static::CREATED_AT, static::UPDATED_AT); return array_merge($this->dates, $defaults); } @@ -2390,7 +2850,7 @@ public function fromDateTime($value) // If this value is some other type of string, we'll create the DateTime with // the format used by the database connection. Once we get the instance we // can return back the finally formatted DateTime instances to the devs. - elseif ( ! $value instanceof DateTime) + else { $value = Carbon::createFromFormat($format, $value); } @@ -2448,11 +2908,18 @@ protected function getDateFormat() /** * Clone the model into a new, non-existing instance. * + * @param array $except * @return \Illuminate\Database\Eloquent\Model */ - public function replicate() + public function replicate(array $except = null) { - $attributes = array_except($this->attributes, array($this->getKeyName())); + $except = $except ?: [ + $this->getKeyName(), + $this->getCreatedAtColumn(), + $this->getUpdatedAtColumn(), + ]; + + $attributes = array_except($this->attributes, $except); with($instance = new static)->setRawAttributes($attributes); @@ -2498,7 +2965,7 @@ public function getOriginal($key = null, $default = null) /** * Sync the original attributes with the current. * - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function syncOriginal() { @@ -2508,14 +2975,38 @@ public function syncOriginal() } /** - * Determine if a given attribute is dirty. + * Sync a single original attribute with its current value. * * @param string $attribute + * @return $this + */ + public function syncOriginalAttribute($attribute) + { + $this->original[$attribute] = $this->attributes[$attribute]; + + return $this; + } + + /** + * Determine if the model or given attribute(s) have been modified. + * + * @param array|string|null $attributes * @return bool */ - public function isDirty($attribute) + public function isDirty($attributes = null) { - return array_key_exists($attribute, $this->getDirty()); + $dirty = $this->getDirty(); + + if (is_null($attributes)) return count($dirty) > 0; + + if ( ! is_array($attributes)) $attributes = func_get_args(); + + foreach ($attributes as $attribute) + { + if (array_key_exists($attribute, $dirty)) return true; + } + + return false; } /** @@ -2529,7 +3020,12 @@ public function getDirty() foreach ($this->attributes as $key => $value) { - if ( ! array_key_exists($key, $this->original) || $value !== $this->original[$key]) + if ( ! array_key_exists($key, $this->original)) + { + $dirty[$key] = $value; + } + elseif ($value !== $this->original[$key] && + ! $this->originalIsNumericallyEquivalent($key)) { $dirty[$key] = $value; } @@ -2538,6 +3034,21 @@ public function getDirty() return $dirty; } + /** + * Determine if the new and old values for a given key are numerically equivalent. + * + * @param string $key + * @return bool + */ + protected function originalIsNumericallyEquivalent($key) + { + $current = $this->attributes[$key]; + + $original = $this->original[$key]; + + return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0; + } + /** * Get all the loaded relations for the instance. * @@ -2564,7 +3075,7 @@ public function getRelation($relation) * * @param string $relation * @param mixed $value - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function setRelation($relation, $value) { @@ -2577,7 +3088,7 @@ public function setRelation($relation, $value) * Set the entire relations array on the model. * * @param array $relations - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function setRelations(array $relations) { @@ -2610,7 +3121,7 @@ public function getConnectionName() * Set the connection associated with the model. * * @param string $name - * @return \Illuminate\Database\Eloquent\Model + * @return $this */ public function setConnection($name) { @@ -2651,10 +3162,20 @@ public static function setConnectionResolver(Resolver $resolver) static::$resolver = $resolver; } + /** + * Unset the connection resolver for models. + * + * @return void + */ + public static function unsetConnectionResolver() + { + static::$resolver = null; + } + /** * Get the event dispatcher instance. * - * @return \Illuminate\Events\Dispatcher + * @return \Illuminate\Contracts\Events\Dispatcher */ public static function getEventDispatcher() { @@ -2664,7 +3185,7 @@ public static function getEventDispatcher() /** * Set the event dispatcher instance. * - * @param \Illuminate\Events\Dispatcher $dispatcher + * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher * @return void */ public static function setEventDispatcher(Dispatcher $dispatcher) @@ -2693,7 +3214,7 @@ public function getMutatedAttributes() if (isset(static::$mutatorCache[$class])) { - return static::$mutatorCache[get_class($this)]; + return static::$mutatorCache[$class]; } return array(); @@ -2771,12 +3292,12 @@ public function offsetUnset($offset) * Determine if an attribute exists on the model. * * @param string $key - * @return void + * @return bool */ public function __isset($key) { return ((isset($this->attributes[$key]) || isset($this->relations[$key])) || - ($this->hasGetMutator($key) && ! is_null($this->getAttributeValue($key)))); + ($this->hasGetMutator($key) && ! is_null($this->getAttributeValue($key)))); } /** @@ -2787,9 +3308,7 @@ public function __isset($key) */ public function __unset($key) { - unset($this->attributes[$key]); - - unset($this->relations[$key]); + unset($this->attributes[$key], $this->relations[$key]); } /** @@ -2835,4 +3354,14 @@ public function __toString() return $this->toJson(); } + /** + * When a model is being unserialized, check if it needs to be booted. + * + * @return void + */ + public function __wakeup() + { + $this->bootIfNotBooted(); + } + } diff --git a/Eloquent/ModelNotFoundException.php b/Eloquent/ModelNotFoundException.php index 25750dc62..84be36ee0 100755 --- a/Eloquent/ModelNotFoundException.php +++ b/Eloquent/ModelNotFoundException.php @@ -1,3 +1,39 @@ model = $model; + + $this->message = "No query results for model [{$model}]."; + + return $this; + } + + /** + * Get the affected Eloquent model. + * + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/Eloquent/QueueEntityResolver.php b/Eloquent/QueueEntityResolver.php new file mode 100644 index 000000000..2dc2be52c --- /dev/null +++ b/Eloquent/QueueEntityResolver.php @@ -0,0 +1,27 @@ +find($id); + + if ($instance) + { + return $instance; + } + + throw new EntityNotFoundException($type, $id); + } + +} diff --git a/Eloquent/Relations/BelongsTo.php b/Eloquent/Relations/BelongsTo.php index eef442070..8cd133761 100755 --- a/Eloquent/Relations/BelongsTo.php +++ b/Eloquent/Relations/BelongsTo.php @@ -1,8 +1,8 @@ select(new Expression('count(*)')); + + $otherKey = $this->wrap($query->getModel()->getTable().'.'.$this->otherKey); + + return $query->where($this->getQualifiedForeignKey(), '=', new Expression($otherKey)); } /** @@ -141,7 +144,7 @@ protected function getEagerModelKeys(array $models) * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -204,6 +207,18 @@ public function associate(Model $model) return $this->parent->setRelation($this->relation, $model); } + /** + * Dissociate previously associated model from the given parent. + * + * @return \Illuminate\Database\Eloquent\Model + */ + public function dissociate() + { + $this->parent->setAttribute($this->foreignKey, null); + + return $this->parent->setRelation($this->relation, null); + } + /** * Update the parent model on the relationship. * @@ -227,4 +242,34 @@ public function getForeignKey() return $this->foreignKey; } -} \ No newline at end of file + /** + * Get the fully qualified foreign key of the relationship. + * + * @return string + */ + public function getQualifiedForeignKey() + { + return $this->parent->getTable().'.'.$this->foreignKey; + } + + /** + * Get the associated key of the relationship. + * + * @return string + */ + public function getOtherKey() + { + return $this->otherKey; + } + + /** + * Get the fully qualified associated key of the relationship. + * + * @return string + */ + public function getQualifiedOtherKeyName() + { + return $this->related->getTable().'.'.$this->otherKey; + } + +} diff --git a/Eloquent/Relations/BelongsToMany.php b/Eloquent/Relations/BelongsToMany.php index a4534379c..0c147c0bf 100755 --- a/Eloquent/Relations/BelongsToMany.php +++ b/Eloquent/Relations/BelongsToMany.php @@ -2,6 +2,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -43,7 +44,14 @@ class BelongsToMany extends Relation { protected $pivotColumns = array(); /** - * Create a new has many relationship instance. + * Any pivot table restrictions. + * + * @var array + */ + protected $pivotWheres = []; + + /** + * Create a new belongs to many relationship instance. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent @@ -84,6 +92,8 @@ public function getResults() */ public function wherePivot($column, $operator = null, $value = null, $boolean = 'and') { + $this->pivotWheres[] = func_get_args(); + return $this->where($this->table.'.'.$column, $operator, $value, $boolean); } @@ -93,7 +103,6 @@ public function wherePivot($column, $operator = null, $value = null, $boolean = * @param string $column * @param string $operator * @param mixed $value - * @param string $boolean * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function orWherePivot($column, $operator = null, $value = null) @@ -140,6 +149,8 @@ public function get($columns = array('*')) // First we'll add the proper select columns onto the query so it is run with // the proper columns. Then, we will get the results and hydrate out pivot // models with the result of those columns as a separate model relation. + $columns = $this->query->getQuery()->columns ? array() : $columns; + $select = $this->getSelectColumns($columns); $models = $this->query->addSelect($select)->getModels(); @@ -157,27 +168,6 @@ public function get($columns = array('*')) return $this->related->newCollection($models); } - /** - * Get a paginator for the "select" statement. - * - * @param int $perPage - * @param array $columns - * @return \Illuminate\Pagination\Paginator - */ - public function paginate($perPage = null, $columns = array('*')) - { - $this->query->addSelect($this->getSelectColumns($columns)); - - // When paginating results, we need to add the pivot columns to the query and - // then hydrate into the pivot objects once the results have been gathered - // from the database since this isn't performed by the Eloquent builder. - $pager = $this->query->paginate($perPage, $columns); - - $this->hydratePivotRelation($pager->getItems()); - - return $pager; - } - /** * Hydrate the pivot table relationship on the models. * @@ -239,18 +229,55 @@ public function addConstraints() * Add the constraints for a relationship count query. * * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent * @return \Illuminate\Database\Eloquent\Builder */ - public function getRelationCountQuery(Builder $query) + public function getRelationCountQuery(Builder $query, Builder $parent) { + if ($parent->getQuery()->from == $query->getQuery()->from) + { + return $this->getRelationCountQueryForSelfJoin($query, $parent); + } + $this->setJoin($query); - return parent::getRelationCountQuery($query); + return parent::getRelationCountQuery($query, $parent); + } + + /** + * Add the constraints for a relationship count query on the same table. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationCountQueryForSelfJoin(Builder $query, Builder $parent) + { + $query->select(new Expression('count(*)')); + + $tablePrefix = $this->query->getQuery()->getConnection()->getTablePrefix(); + + $query->from($this->table.' as '.$tablePrefix.$hash = $this->getRelationCountHash()); + + $key = $this->wrap($this->getQualifiedParentKeyName()); + + return $query->where($hash.'.'.$this->foreignKey, '=', new Expression($key)); + } + + /** + * Get a relationship join table hash. + * + * @return string + */ + public function getRelationCountHash() + { + return 'self_'.md5(microtime(true)); } /** * Set the select clause for the relation query. * + * @param array $columns * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ protected function getSelectColumns(array $columns = array('*')) @@ -285,11 +312,22 @@ protected function getAliasedPivotColumns() return array_unique($columns); } + /** + * Determine whether the given column is defined as a pivot column. + * + * @param string $column + * @return bool + */ + protected function hasPivotColumn($column) + { + return in_array($column, $this->pivotColumns); + } + /** * Set the join clause for the relation query. * * @param \Illuminate\Database\Eloquent\Builder|null - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return $this */ protected function setJoin($query = null) { @@ -310,7 +348,7 @@ protected function setJoin($query = null) /** * Set the where clause for the relation query. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return $this */ protected function setWhere() { @@ -337,7 +375,7 @@ public function addEagerConstraints(array $models) * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -474,6 +512,76 @@ public function saveMany(array $models, array $joinings = array()) return $models; } + /** + * Find a related model by its primary key or return new instance of the related model. + * + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model + */ + public function findOrNew($id, $columns = ['*']) + { + if (is_null($instance = $this->find($id, $columns))) + { + $instance = $this->getRelated()->newInstance(); + } + + return $instance; + } + + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrNew(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) + { + $instance = $this->related->newInstance(); + } + + return $instance; + } + + /** + * Get the first related record matching the attributes or create it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrCreate(array $attributes, array $joining = [], $touch = true) + { + if (is_null($instance = $this->where($attributes)->first())) + { + $instance = $this->create($attributes, $joining, $touch); + } + + return $instance; + } + + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model + */ + public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) + { + if (is_null($instance = $this->where($attributes)->first())) + { + return $this->create($values, $joining, $touch); + } + + $instance->fill($values); + + $instance->save(['touch' => false]); + + return $instance; + } + /** * Create a new instance of the related model. * @@ -518,14 +626,20 @@ public function createMany(array $records, array $joinings = array()) } /** - * Sync the intermediate tables with a list of IDs. + * Sync the intermediate tables with a list of IDs or collection of models. * * @param array $ids * @param bool $detaching - * @return void + * @return array */ - public function sync(array $ids, $detaching = true) + public function sync($ids, $detaching = true) { + $changes = array( + 'attached' => array(), 'detached' => array(), 'updated' => array() + ); + + if ($ids instanceof Collection) $ids = $ids->modelKeys(); + // First we need to attach any of the associated models that are not currently // in this joining table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. @@ -541,14 +655,23 @@ public function sync(array $ids, $detaching = true) if ($detaching && count($detach) > 0) { $this->detach($detach); + + $changes['detached'] = (array) array_map(function($v) { return (int) $v; }, $detach); } // Now we are finally ready to attach the new records. Note that we'll disable // touching until after the entire operation is complete so we don't fire a // ton of touch operations until we are totally done syncing the records. - $this->attachNew($records, $current, false); + $changes = array_merge( + $changes, $this->attachNew($records, $current, false) + ); - $this->touchIfTouching(); + if (count($changes['attached']) || count($changes['updated'])) + { + $this->touchIfTouching(); + } + + return $changes; } /** @@ -580,10 +703,12 @@ protected function formatSyncList(array $records) * @param array $records * @param array $current * @param bool $touch - * @return void + * @return array */ protected function attachNew(array $records, array $current, $touch = true) { + $changes = array('attached' => array(), 'updated' => array()); + foreach ($records as $id => $attributes) { // If the ID is not in the list of existing pivot IDs, we will insert a new pivot @@ -592,12 +717,21 @@ protected function attachNew(array $records, array $current, $touch = true) if ( ! in_array($id, $current)) { $this->attach($id, $attributes, $touch); + + $changes['attached'][] = (int) $id; } - elseif (count($attributes) > 0) + + // Now we'll try to update an existing pivot record with the attributes that were + // given to the method. If the model is actually updated we will add it to the + // list of updated pivot records so we return them back out to the consumer. + elseif (count($attributes) > 0 && + $this->updateExistingPivot($id, $attributes, $touch)) { - $this->updateExistingPivot($id, $attributes, $touch); + $changes['updated'][] = (int) $id; } } + + return $changes; } /** @@ -608,16 +742,18 @@ protected function attachNew(array $records, array $current, $touch = true) * @param bool $touch * @return void */ - public function updateExistingPivot($id, array $attributes, $touch) + public function updateExistingPivot($id, array $attributes, $touch = true) { if (in_array($this->updatedAt(), $this->pivotColumns)) { $attributes = $this->setTimestampsOnAttach($attributes, true); } - $this->newPivotStatementForId($id)->update($attributes); + $updated = $this->newPivotStatementForId($id)->update($attributes); if ($touch) $this->touchIfTouching(); + + return $updated; } /** @@ -643,13 +779,15 @@ public function attach($id, array $attributes = array(), $touch = true) * Create an array of records to insert into the pivot table. * * @param array $ids - * @return void + * @param array $attributes + * @return array */ protected function createAttachRecords($ids, array $attributes) { $records = array(); - $timed = in_array($this->createdAt(), $this->pivotColumns); + $timed = ($this->hasPivotColumn($this->createdAt()) || + $this->hasPivotColumn($this->updatedAt())); // To create the attachment records, we will simply spin through the IDs given // and create a new record to insert for each ID. Each ID may actually be a @@ -697,10 +835,8 @@ protected function getAttachId($key, $value, array $attributes) { return array($key, array_merge($value, $attributes)); } - else - { - return array($value, $attributes); - } + + return array($value, $attributes); } /** @@ -728,7 +864,7 @@ protected function createAttachRecord($id, $timed) } /** - * Set the creation and update timstamps on an attach record. + * Set the creation and update timestamps on an attach record. * * @param array $record * @param bool $exists @@ -738,9 +874,15 @@ protected function setTimestampsOnAttach(array $record, $exists = false) { $fresh = $this->parent->freshTimestamp(); - if ( ! $exists) $record[$this->createdAt()] = $fresh; + if ( ! $exists && $this->hasPivotColumn($this->createdAt())) + { + $record[$this->createdAt()] = $fresh; + } - $record[$this->updatedAt()] = $fresh; + if ($this->hasPivotColumn($this->updatedAt())) + { + $record[$this->updatedAt()] = $fresh; + } return $record; } @@ -765,7 +907,7 @@ public function detach($ids = array(), $touch = true) if (count($ids) > 0) { - $query->whereIn($this->otherKey, $ids); + $query->whereIn($this->otherKey, (array) $ids); } if ($touch) $this->touchIfTouching(); @@ -785,9 +927,9 @@ public function detach($ids = array(), $touch = true) */ public function touchIfTouching() { - if ($this->touchingParent()) $this->getParent()->touch(); + if ($this->touchingParent()) $this->getParent()->touch(); - if ($this->getParent()->touches($this->relationName)) $this->touch(); + if ($this->getParent()->touches($this->relationName)) $this->touch(); } /** @@ -807,7 +949,7 @@ protected function touchingParent() */ protected function guessInverseRelation() { - return strtolower(str_plural(class_basename($this->getParent()))); + return camel_case(str_plural(class_basename($this->getParent()))); } /** @@ -819,6 +961,11 @@ protected function newPivotQuery() { $query = $this->newPivotStatement(); + foreach ($this->pivotWheres as $whereArgs) + { + call_user_func_array([$query, 'where'], $whereArgs); + } + return $query->where($this->foreignKey, $this->parent->getKey()); } @@ -840,11 +987,7 @@ public function newPivotStatement() */ public function newPivotStatementForId($id) { - $pivot = $this->newPivotStatement(); - - $key = $this->parent->getKey(); - - return $pivot->where($this->foreignKey, $key)->where($this->otherKey, $id); + return $this->newPivotQuery()->where($this->otherKey, $id); } /** @@ -852,7 +995,7 @@ public function newPivotStatementForId($id) * * @param array $attributes * @param bool $exists - * @return \Illuminate\Database\Eloquent\Relation\Pivot + * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newPivot(array $attributes = array(), $exists = false) { @@ -875,8 +1018,8 @@ public function newExistingPivot(array $attributes = array()) /** * Set the columns on the pivot table to retrieve. * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @param mixed $columns + * @return $this */ public function withPivot($columns) { @@ -890,11 +1033,13 @@ public function withPivot($columns) /** * Specify that the pivot table has creation and update timestamps. * + * @param mixed $createdAt + * @param mixed $updatedAt * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ - public function withTimestamps() + public function withTimestamps($createdAt = null, $updatedAt = null) { - return $this->withPivot($this->createdAt(), $this->updatedAt()); + return $this->withPivot($createdAt ?: $this->createdAt(), $updatedAt ?: $this->updatedAt()); } /** @@ -908,7 +1053,7 @@ public function getRelatedFreshUpdate() } /** - * Get the key for comparing against the pareny key in "has" query. + * Get the key for comparing against the parent key in "has" query. * * @return string */ @@ -938,23 +1083,23 @@ public function getOtherKey() } /** - * Get the fully qualified parent key naem. + * Get the intermediate table for the relationship. * * @return string */ - protected function getQualifiedParentKeyName() + public function getTable() { - return $this->parent->getQualifiedKeyName(); + return $this->table; } /** - * Get the intermediate table for the relationship. + * Get the relationship name for the relationship. * * @return string */ - public function getTable() + public function getRelationName() { - return $this->table; + return $this->relationName; } } diff --git a/Eloquent/Relations/HasMany.php b/Eloquent/Relations/HasMany.php index 171858258..159a65820 100755 --- a/Eloquent/Relations/HasMany.php +++ b/Eloquent/Relations/HasMany.php @@ -19,7 +19,7 @@ public function getResults() * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -44,4 +44,4 @@ public function match(array $models, Collection $results, $relation) return $this->matchMany($models, $results, $relation); } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/HasManyThrough.php b/Eloquent/Relations/HasManyThrough.php index 3f6bfed55..a53a24674 100644 --- a/Eloquent/Relations/HasManyThrough.php +++ b/Eloquent/Relations/HasManyThrough.php @@ -2,22 +2,40 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Eloquent\Collection; class HasManyThrough extends Relation { + /** + * The distance parent model instance. + * + * @var \Illuminate\Database\Eloquent\Model + */ protected $farParent; + /** + * The near key on the relationship. + * + * @var string + */ protected $firstKey; + /** + * The far key on the relationship. + * + * @var string + */ protected $secondKey; /** - * Create a new has many relationship instance. + * Create a new has many through relationship instance. * * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $farParent * @param \Illuminate\Database\Eloquent\Model $parent - * @param string $foreignKey + * @param string $firstKey + * @param string $secondKey * @return void */ public function __construct(Builder $query, Model $farParent, Model $parent, $firstKey, $secondKey) @@ -50,19 +68,26 @@ public function addConstraints() * Add the constraints for a relationship count query. * * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent * @return \Illuminate\Database\Eloquent\Builder */ - public function getRelationCountQuery(Builder $query) + public function getRelationCountQuery(Builder $query, Builder $parent) { + $parentTable = $this->parent->getTable(); + $this->setJoin($query); - return parent::getRelationCountQuery($query); + $query->select(new Expression('count(*)')); + + $key = $this->wrap($parentTable.'.'.$this->firstKey); + + return $query->where($this->getHasCompareKey(), '=', new Expression($key)); } /** * Set the join clause on the query. * - * @param \Illuminate\Databaes\Eloquent\Builder|null $query + * @param \Illuminate\Database\Eloquent\Builder|null $query * @return void */ protected function setJoin(Builder $query = null) @@ -92,7 +117,7 @@ public function addEagerConstraints(array $models) * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -144,7 +169,7 @@ protected function buildDictionary(Collection $results) { $dictionary = array(); - $foreign = $this->farParent->getForeignKey(); + $foreign = $this->firstKey; // First we will create a dictionary of models keyed by the foreign key of the // relationship as this will allow us to quickly access all of the related @@ -196,6 +221,7 @@ public function get($columns = array('*')) /** * Set the select clause for the relation query. * + * @param array $columns * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ protected function getSelectColumns(array $columns = array('*')) @@ -208,18 +234,22 @@ protected function getSelectColumns(array $columns = array('*')) return array_merge($columns, array($this->parent->getTable().'.'.$this->firstKey)); } - /** - * Get the key name of the parent model. + /* + * Get a paginator for the "select" statement. * - * @return string + * @param int $perPage + * @param array $columns + * @return \Illuminate\Pagination\Paginator */ - protected function getQualifiedParentKeyName() + public function paginate($perPage = null, $columns = array('*')) { - return $this->parent->getQualifiedKeyName(); + $this->query->addSelect($this->getSelectColumns($columns)); + + return $this->query->paginate($perPage, $columns); } /** - * Get the key for comparing against the pareny key in "has" query. + * Get the key for comparing against the parent key in "has" query. * * @return string */ @@ -228,4 +258,4 @@ public function getHasCompareKey() return $this->farParent->getQualifiedKeyName(); } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/HasOne.php b/Eloquent/Relations/HasOne.php index 69437c2ad..fd0f9a022 100755 --- a/Eloquent/Relations/HasOne.php +++ b/Eloquent/Relations/HasOne.php @@ -19,7 +19,7 @@ public function getResults() * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -44,4 +44,4 @@ public function match(array $models, Collection $results, $relation) return $this->matchOne($models, $results, $relation); } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/HasOneOrMany.php b/Eloquent/Relations/HasOneOrMany.php index 9d2664540..db6d1fdf6 100755 --- a/Eloquent/Relations/HasOneOrMany.php +++ b/Eloquent/Relations/HasOneOrMany.php @@ -21,11 +21,12 @@ abstract class HasOneOrMany extends Relation { protected $localKey; /** - * Create a new has many relationship instance. + * Create a new has one or many relationship instance. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent * @param string $foreignKey + * @param string $localKey * @return void */ public function __construct(Builder $query, Model $parent, $foreignKey, $localKey) @@ -57,7 +58,7 @@ public function addConstraints() */ public function addEagerConstraints(array $models) { - $this->query->whereIn($this->foreignKey, $this->getKeys($models)); + $this->query->whereIn($this->foreignKey, $this->getKeys($models, $this->localKey)); } /** @@ -181,6 +182,77 @@ public function saveMany(array $models) return $models; } + /** + * Find a model by its primary key or return new instance of the related model. + * + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model + */ + public function findOrNew($id, $columns = ['*']) + { + if (is_null($instance = $this->find($id, $columns))) + { + $instance = $this->related->newInstance(); + + $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); + } + + return $instance; + } + + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrNew(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) + { + $instance = $this->related->newInstance(); + + $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); + } + + return $instance; + } + + /** + * Get the first related record matching the attributes or create it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrCreate(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) + { + $instance = $this->create($attributes); + } + + return $instance; + } + + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model + */ + public function updateOrCreate(array $attributes, array $values = []) + { + $instance = $this->firstOrNew($attributes); + + $instance->fill($values); + + $instance->save(); + + return $instance; + } + /** * Create a new instance of the related model. * @@ -189,16 +261,12 @@ public function saveMany(array $models) */ public function create(array $attributes) { - $foreign = array( - $this->getPlainForeignKey() => $this->getParentKey(), - ); - // Here we will set the raw attributes to avoid hitting the "fill" method so // that we do not have to worry about a mass accessor rules blocking sets // on the models. Otherwise, some of these attributes will not get set. - $instance = $this->related->newInstance(); + $instance = $this->related->newInstance($attributes); - $instance->setRawAttributes(array_merge($attributes, $foreign)); + $instance->setAttribute($this->getPlainForeignKey(), $this->getParentKey()); $instance->save(); @@ -233,14 +301,14 @@ public function update(array $attributes) { if ($this->related->usesTimestamps()) { - $attributes[$this->relatedUpdatedAt()] = $this->related->freshTimestamp(); + $attributes[$this->relatedUpdatedAt()] = $this->related->freshTimestampString(); } return $this->query->update($attributes); } /** - * Get the key for comparing against the pareny key in "has" query. + * Get the key for comparing against the parent key in "has" query. * * @return string */ @@ -272,23 +340,23 @@ public function getPlainForeignKey() } /** - * Get the key value of the paren's local key. + * Get the key value of the parent's local key. * * @return mixed */ - protected function getParentKey() + public function getParentKey() { return $this->parent->getAttribute($this->localKey); } /** - * Get the fully qualified parent key naem. + * Get the fully qualified parent key name. * * @return string */ - protected function getQualifiedParentKeyName() + public function getQualifiedParentKeyName() { return $this->parent->getTable().'.'.$this->localKey; } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/MorphMany.php b/Eloquent/Relations/MorphMany.php index 710fab5fb..1abdf3797 100755 --- a/Eloquent/Relations/MorphMany.php +++ b/Eloquent/Relations/MorphMany.php @@ -19,7 +19,7 @@ public function getResults() * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -44,4 +44,4 @@ public function match(array $models, Collection $results, $relation) return $this->matchMany($models, $results, $relation); } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/MorphOne.php b/Eloquent/Relations/MorphOne.php index 9d00c539b..fdebc24ee 100755 --- a/Eloquent/Relations/MorphOne.php +++ b/Eloquent/Relations/MorphOne.php @@ -19,7 +19,7 @@ public function getResults() * * @param array $models * @param string $relation - * @return void + * @return array */ public function initRelation(array $models, $relation) { @@ -44,4 +44,4 @@ public function match(array $models, Collection $results, $relation) return $this->matchOne($models, $results, $relation); } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/MorphOneOrMany.php b/Eloquent/Relations/MorphOneOrMany.php index cf18da984..19a98172c 100755 --- a/Eloquent/Relations/MorphOneOrMany.php +++ b/Eloquent/Relations/MorphOneOrMany.php @@ -20,7 +20,7 @@ abstract class MorphOneOrMany extends HasOneOrMany { protected $morphClass; /** - * Create a new has many relationship instance. + * Create a new morph one or many relationship instance. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent @@ -33,7 +33,7 @@ public function __construct(Builder $query, Model $parent, $type, $id, $localKey { $this->morphType = $type; - $this->morphClass = get_class($parent); + $this->morphClass = $parent->getMorphClass(); parent::__construct($query, $parent, $id, $localKey); } @@ -54,14 +54,15 @@ public function addConstraints() } /** - * Add the constraints for a relationship count query. + * Get the relationship count query. * * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent * @return \Illuminate\Database\Eloquent\Builder */ - public function getRelationCountQuery(Builder $query) + public function getRelationCountQuery(Builder $query, Builder $parent) { - $query = parent::getRelationCountQuery($query); + $query = parent::getRelationCountQuery($query, $parent); return $query->where($this->morphType, $this->morphClass); } @@ -92,6 +93,83 @@ public function save(Model $model) return parent::save($model); } + /** + * Find a related model by its primary key or return new instance of the related model. + * + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model + */ + public function findOrNew($id, $columns = ['*']) + { + if (is_null($instance = $this->find($id, $columns))) + { + $instance = $this->related->newInstance(); + + // When saving a polymorphic relationship, we need to set not only the foreign + // key, but also the foreign key type, which is typically the class name of + // the parent model. This makes the polymorphic item unique in the table. + $this->setForeignAttributesForCreate($instance); + } + + return $instance; + } + + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrNew(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) + { + $instance = $this->related->newInstance(); + + // When saving a polymorphic relationship, we need to set not only the foreign + // key, but also the foreign key type, which is typically the class name of + // the parent model. This makes the polymorphic item unique in the table. + $this->setForeignAttributesForCreate($instance); + } + + return $instance; + } + + /** + * Get the first related record matching the attributes or create it. + * + * @param array $attributes + * @return \Illuminate\Database\Eloquent\Model + */ + public function firstOrCreate(array $attributes) + { + if (is_null($instance = $this->where($attributes)->first())) + { + $instance = $this->create($attributes); + } + + return $instance; + } + + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model + */ + public function updateOrCreate(array $attributes, array $values = []) + { + $instance = $this->firstOrNew($attributes); + + $instance->fill($values); + + $instance->save(); + + return $instance; + } + /** * Create a new instance of the related model. * @@ -100,14 +178,12 @@ public function save(Model $model) */ public function create(array $attributes) { - $foreign = $this->getForeignAttributesForCreate(); + $instance = $this->related->newInstance($attributes); // When saving a polymorphic relationship, we need to set not only the foreign // key, but also the foreign key type, which is typically the class name of // the parent model. This makes the polymorphic item unique in the table. - $attributes = array_merge($attributes, $foreign); - - $instance = $this->related->newInstance($attributes); + $this->setForeignAttributesForCreate($instance); $instance->save(); @@ -115,17 +191,16 @@ public function create(array $attributes) } /** - * Get the foreign ID and type for creating a related model. + * Set the foreign ID and type for creating a related model. * - * @return array + * @param \Illuminate\Database\Eloquent\Model $model + * @return void */ - protected function getForeignAttributesForCreate() + protected function setForeignAttributesForCreate(Model $model) { - $foreign = array($this->getPlainForeignKey() => $this->getParentKey()); - - $foreign[last(explode('.', $this->morphType))] = $this->morphClass; + $model->{$this->getPlainForeignKey()} = $this->getParentKey(); - return $foreign; + $model->{last(explode('.', $this->morphType))} = $this->morphClass; } /** @@ -158,4 +233,4 @@ public function getMorphClass() return $this->morphClass; } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/MorphPivot.php b/Eloquent/Relations/MorphPivot.php index 85ffbc8aa..fe8c644b2 100644 --- a/Eloquent/Relations/MorphPivot.php +++ b/Eloquent/Relations/MorphPivot.php @@ -4,15 +4,33 @@ class MorphPivot extends Pivot { + /** + * The type of the polymorphic relation. + * + * Explicitly define this so it's not included in saved attributes. + * + * @var string + */ + protected $morphType; + + /** + * The value of the polymorphic relation. + * + * Explicitly define this so it's not included in saved attributes. + * + * @var string + */ + protected $morphClass; + /** * Set the keys for a save update query. * - * @param \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ protected function setKeysForSaveQuery(Builder $query) { - $query->where($this->morphType, $this->getAttribute($this->morphType)); + $query->where($this->morphType, $this->morphClass); return parent::setKeysForSaveQuery($query); } @@ -26,7 +44,7 @@ public function delete() { $query = $this->getDeleteQuery(); - $query->where($this->morphType, $this->getAttribute($this->morphType)); + $query->where($this->morphType, $this->morphClass); return $query->delete(); } @@ -35,7 +53,7 @@ public function delete() * Set the morph type for the pivot. * * @param string $morphType - * @return \Illuminate\Database\Eloquent\Relations\MorphPivot + * @return $this */ public function setMorphType($morphType) { @@ -44,4 +62,17 @@ public function setMorphType($morphType) return $this; } + /** + * Set the morph class for the pivot. + * + * @param string $morphClass + * @return \Illuminate\Database\Eloquent\Relations\MorphPivot + */ + public function setMorphClass($morphClass) + { + $this->morphClass = $morphClass; + + return $this; + } + } diff --git a/Eloquent/Relations/MorphTo.php b/Eloquent/Relations/MorphTo.php new file mode 100644 index 000000000..c401a9c0c --- /dev/null +++ b/Eloquent/Relations/MorphTo.php @@ -0,0 +1,246 @@ +morphType = $type; + + parent::__construct($query, $parent, $foreignKey, $otherKey, $relation); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + $this->buildDictionary($this->models = Collection::make($models)); + } + + /** + * Build a dictionary with the models. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @return void + */ + protected function buildDictionary(Collection $models) + { + foreach ($models as $model) + { + if ($model->{$this->morphType}) + { + $this->dictionary[$model->{$this->morphType}][$model->{$this->foreignKey}][] = $model; + } + } + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + return $models; + } + + /** + * Associate the model instance to the given parent. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return \Illuminate\Database\Eloquent\Model + */ + public function associate(Model $model) + { + $this->parent->setAttribute($this->foreignKey, $model->getKey()); + + $this->parent->setAttribute($this->morphType, $model->getMorphClass()); + + return $this->parent->setRelation($this->relation, $model); + } + + /** + * Get the results of the relationship. + * + * Called via eager load method of Eloquent query builder. + * + * @return mixed + */ + public function getEager() + { + foreach (array_keys($this->dictionary) as $type) + { + $this->matchToMorphParents($type, $this->getResultsByType($type)); + } + + return $this->models; + } + + /** + * Match the results for a given type to their parents. + * + * @param string $type + * @param \Illuminate\Database\Eloquent\Collection $results + * @return void + */ + protected function matchToMorphParents($type, Collection $results) + { + foreach ($results as $result) + { + if (isset($this->dictionary[$type][$result->getKey()])) + { + foreach ($this->dictionary[$type][$result->getKey()] as $model) + { + $model->setRelation($this->relation, $result); + } + } + } + } + + /** + * Get all of the relation results for a type. + * + * @param string $type + * @return \Illuminate\Database\Eloquent\Collection + */ + protected function getResultsByType($type) + { + $instance = $this->createModelByType($type); + + $key = $instance->getKeyName(); + + $query = $instance->newQuery(); + + $query = $this->useWithTrashed($query); + + return $query->whereIn($key, $this->gatherKeysByType($type)->all())->get(); + } + + /** + * Gather all of the foreign keys for a given type. + * + * @param string $type + * @return array + */ + protected function gatherKeysByType($type) + { + $foreign = $this->foreignKey; + + return BaseCollection::make($this->dictionary[$type])->map(function($models) use ($foreign) + { + return head($models)->{$foreign}; + + })->unique(); + } + + /** + * Create a new model instance by type. + * + * @param string $type + * @return \Illuminate\Database\Eloquent\Model + */ + public function createModelByType($type) + { + return new $type; + } + + /** + * Get the foreign key "type" name. + * + * @return string + */ + public function getMorphType() + { + return $this->morphType; + } + + /** + * Get the dictionary used by the relationship. + * + * @return array + */ + public function getDictionary() + { + return $this->dictionary; + } + + /** + * Fetch soft-deleted model instances with query + * + * @return $this + */ + public function withTrashed() + { + $this->withTrashed = true; + + $this->query = $this->useWithTrashed($this->query); + + return $this; + } + + /** + * Return trashed models with query if told so + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function useWithTrashed(Builder $query) + { + if ($this->withTrashed && $query->getMacro('withTrashed') !== null) + { + return $query->withTrashed(); + } + return $query; + } + +} diff --git a/Eloquent/Relations/MorphToMany.php b/Eloquent/Relations/MorphToMany.php index 26b5dc852..18d970d92 100644 --- a/Eloquent/Relations/MorphToMany.php +++ b/Eloquent/Relations/MorphToMany.php @@ -29,7 +29,7 @@ class MorphToMany extends BelongsToMany { protected $inverse; /** - * Create a new has many relationship instance. + * Create a new morph to many relationship instance. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent @@ -45,7 +45,7 @@ public function __construct(Builder $query, Model $parent, $name, $table, $forei { $this->inverse = $inverse; $this->morphType = $name.'_type'; - $this->morphClass = $inverse ? get_class($query->getModel()) : get_class($parent); + $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass(); parent::__construct($query, $parent, $table, $foreignKey, $otherKey, $relationName); } @@ -53,7 +53,7 @@ public function __construct(Builder $query, Model $parent, $name, $table, $forei /** * Set the where clause for the relation query. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return $this */ protected function setWhere() { @@ -64,6 +64,20 @@ protected function setWhere() return $this; } + /** + * Add the constraints for a relationship count query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationCountQuery(Builder $query, Builder $parent) + { + $query = parent::getRelationCountQuery($query, $parent); + + return $query->where($this->table.'.'.$this->morphType, $this->morphClass); + } + /** * Set the constraints for an eager load of the relation. * @@ -108,17 +122,37 @@ protected function newPivotQuery() * * @param array $attributes * @param bool $exists - * @return \Illuminate\Database\Eloquent\Relation\Pivot + * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newPivot(array $attributes = array(), $exists = false) { $pivot = new MorphPivot($this->parent, $attributes, $this->table, $exists); - $pivot->setPivotKeys($this->foreignKey, $this->otherKey); - - $pivot->setMorphType($this->morphType); + $pivot->setPivotKeys($this->foreignKey, $this->otherKey) + ->setMorphType($this->morphType) + ->setMorphClass($this->morphClass); return $pivot; } -} \ No newline at end of file + /** + * Get the foreign key "type" name. + * + * @return string + */ + public function getMorphType() + { + return $this->morphType; + } + + /** + * Get the class name of the parent model. + * + * @return string + */ + public function getMorphClass() + { + return $this->morphClass; + } + +} diff --git a/Eloquent/Relations/Pivot.php b/Eloquent/Relations/Pivot.php index b36aafac5..365477e05 100755 --- a/Eloquent/Relations/Pivot.php +++ b/Eloquent/Relations/Pivot.php @@ -49,7 +49,7 @@ public function __construct(Model $parent, $attributes, $table, $exists = false) // The pivot model is a "dynamic" model since we will set the tables dynamically // for the instance. This allows it work for any intermediate tables for the // many to many relationship that are defined by this developer's classes. - $this->setRawAttributes($attributes); + $this->setRawAttributes($attributes, true); $this->setTable($table); @@ -99,7 +99,7 @@ protected function getDeleteQuery() $query = $this->newQuery()->where($this->foreignKey, $foreign); - return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); + return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); } /** @@ -127,7 +127,7 @@ public function getOtherKey() * * @param string $foreignKey * @param string $otherKey - * @return \Illuminate\Database\Eloquent\Relations\Pivot + * @return $this */ public function setPivotKeys($foreignKey, $otherKey) { @@ -168,4 +168,4 @@ public function getUpdatedAtColumn() return $this->parent->getUpdatedAtColumn(); } -} \ No newline at end of file +} diff --git a/Eloquent/Relations/Relation.php b/Eloquent/Relations/Relation.php index 953815e9e..86d05392d 100755 --- a/Eloquent/Relations/Relation.php +++ b/Eloquent/Relations/Relation.php @@ -72,7 +72,7 @@ abstract public function addEagerConstraints(array $models); * * @param array $models * @param string $relation - * @return void + * @return array */ abstract public function initRelation(array $models, $relation); @@ -94,25 +94,25 @@ abstract public function match(array $models, Collection $results, $relation); abstract public function getResults(); /** - * Touch all of the related models for the relationship. + * Get the relationship for eager loading. * - * @return void + * @return \Illuminate\Database\Eloquent\Collection */ - public function touch() + public function getEager() { - $column = $this->getRelated()->getUpdatedAtColumn(); - - $this->rawUpdate(array($column => $this->getRelated()->freshTimestampString())); + return $this->get(); } /** - * Restore all of the soft deleted related models. + * Touch all of the related models for the relationship. * - * @return int + * @return void */ - public function restore() + public function touch() { - return $this->query->withTrashed()->restore(); + $column = $this->getRelated()->getUpdatedAtColumn(); + + $this->rawUpdate(array($column => $this->getRelated()->freshTimestampString())); } /** @@ -130,9 +130,10 @@ public function rawUpdate(array $attributes = array()) * Add the constraints for a relationship count query. * * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parent * @return \Illuminate\Database\Eloquent\Builder */ - public function getRelationCountQuery(Builder $query) + public function getRelationCountQuery(Builder $query, Builder $parent) { $query->select(new Expression('count(*)')); @@ -142,7 +143,7 @@ public function getRelationCountQuery(Builder $query) } /** - * Run a callback with constrains disabled on the relation. + * Run a callback with constraints disabled on the relation. * * @param \Closure $callback * @return mixed @@ -164,16 +165,17 @@ public static function noConstraints(Closure $callback) /** * Get all of the primary keys for an array of models. * - * @param array $models + * @param array $models + * @param string $key * @return array */ - protected function getKeys(array $models) + protected function getKeys(array $models, $key = null) { - return array_values(array_map(function($value) + return array_unique(array_values(array_map(function($value) use ($key) { - return $value->getKey(); + return $key ? $value->getAttribute($key) : $value->getKey(); - }, $models)); + }, $models))); } /** @@ -207,11 +209,11 @@ public function getParent() } /** - * Get the fully qualified parent key naem. + * Get the fully qualified parent key name. * * @return string */ - protected function getQualifiedParentKeyName() + public function getQualifiedParentKeyName() { return $this->parent->getQualifiedKeyName(); } @@ -283,4 +285,4 @@ public function __call($method, $parameters) return $result; } -} \ No newline at end of file +} diff --git a/Eloquent/ScopeInterface.php b/Eloquent/ScopeInterface.php new file mode 100644 index 000000000..7cc13494f --- /dev/null +++ b/Eloquent/ScopeInterface.php @@ -0,0 +1,24 @@ +forceDeleting = true; + + $this->delete(); + + $this->forceDeleting = false; + } + + /** + * Perform the actual delete query on this model instance. + * + * @return void + */ + protected function performDeleteOnModel() + { + if ($this->forceDeleting) + { + return $this->withTrashed()->where($this->getKeyName(), $this->getKey())->forceDelete(); + } + + return $this->runSoftDelete(); + } + + /** + * Perform the actual delete query on this model instance. + * + * @return void + */ + protected function runSoftDelete() + { + $query = $this->newQuery()->where($this->getKeyName(), $this->getKey()); + + $this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp(); + + $query->update(array($this->getDeletedAtColumn() => $this->fromDateTime($time))); + } + + /** + * Restore a soft-deleted model instance. + * + * @return bool|null + */ + public function restore() + { + // If the restoring event does not return false, we will proceed with this + // restore operation. Otherwise, we bail out so the developer will stop + // the restore totally. We will clear the deleted timestamp and save. + if ($this->fireModelEvent('restoring') === false) + { + return false; + } + + $this->{$this->getDeletedAtColumn()} = null; + + // Once we have saved the model, we will fire the "restored" event so this + // developer will do anything they need to after a restore operation is + // totally finished. Then we will return the result of the save call. + $this->exists = true; + + $result = $this->save(); + + $this->fireModelEvent('restored', false); + + return $result; + } + + /** + * Determine if the model instance has been soft-deleted. + * + * @return bool + */ + public function trashed() + { + return ! is_null($this->{$this->getDeletedAtColumn()}); + } + + /** + * Get a new query builder that includes soft deletes. + * + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public static function withTrashed() + { + return (new static)->newQueryWithoutScope(new SoftDeletingScope); + } + + /** + * Get a new query builder that only includes soft deletes. + * + * @return \Illuminate\Database\Eloquent\Builder|static + */ + public static function onlyTrashed() + { + $instance = new static; + + $column = $instance->getQualifiedDeletedAtColumn(); + + return $instance->newQueryWithoutScope(new SoftDeletingScope)->whereNotNull($column); + } + + /** + * Register a restoring model event with the dispatcher. + * + * @param \Closure|string $callback + * @return void + */ + public static function restoring($callback) + { + static::registerModelEvent('restoring', $callback); + } + + /** + * Register a restored model event with the dispatcher. + * + * @param \Closure|string $callback + * @return void + */ + public static function restored($callback) + { + static::registerModelEvent('restored', $callback); + } + + /** + * Get the name of the "deleted at" column. + * + * @return string + */ + public function getDeletedAtColumn() + { + return defined('static::DELETED_AT') ? static::DELETED_AT : 'deleted_at'; + } + + /** + * Get the fully qualified "deleted at" column. + * + * @return string + */ + public function getQualifiedDeletedAtColumn() + { + return $this->getTable().'.'.$this->getDeletedAtColumn(); + } + +} diff --git a/Eloquent/SoftDeletingScope.php b/Eloquent/SoftDeletingScope.php new file mode 100644 index 000000000..b4d26ac62 --- /dev/null +++ b/Eloquent/SoftDeletingScope.php @@ -0,0 +1,172 @@ +whereNull($model->getQualifiedDeletedAtColumn()); + + $this->extend($builder); + } + + /** + * Remove the scope from the given Eloquent query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function remove(Builder $builder, Model $model) + { + $column = $model->getQualifiedDeletedAtColumn(); + + $query = $builder->getQuery(); + + foreach ((array) $query->wheres as $key => $where) + { + // If the where clause is a soft delete date constraint, we will remove it from + // the query and reset the keys on the wheres. This allows this developer to + // include deleted model in a relationship result set that is lazy loaded. + if ($this->isSoftDeleteConstraint($where, $column)) + { + unset($query->wheres[$key]); + + $query->wheres = array_values($query->wheres); + } + } + } + + /** + * Extend the query builder with the needed functions. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + public function extend(Builder $builder) + { + foreach ($this->extensions as $extension) + { + $this->{"add{$extension}"}($builder); + } + + $builder->onDelete(function(Builder $builder) + { + $column = $this->getDeletedAtColumn($builder); + + return $builder->update(array( + $column => $builder->getModel()->freshTimestampString() + )); + }); + } + + /** + * Get the "deleted at" column for the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return string + */ + protected function getDeletedAtColumn(Builder $builder) + { + if (count($builder->getQuery()->joins) > 0) + { + return $builder->getModel()->getQualifiedDeletedAtColumn(); + } + else + { + return $builder->getModel()->getDeletedAtColumn(); + } + } + + /** + * Add the force delete extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addForceDelete(Builder $builder) + { + $builder->macro('forceDelete', function(Builder $builder) + { + return $builder->getQuery()->delete(); + }); + } + + /** + * Add the restore extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addRestore(Builder $builder) + { + $builder->macro('restore', function(Builder $builder) + { + $builder->withTrashed(); + + return $builder->update(array($builder->getModel()->getDeletedAtColumn() => null)); + }); + } + + /** + * Add the with-trashed extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addWithTrashed(Builder $builder) + { + $builder->macro('withTrashed', function(Builder $builder) + { + $this->remove($builder, $builder->getModel()); + + return $builder; + }); + } + + /** + * Add the only-trashed extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void + */ + protected function addOnlyTrashed(Builder $builder) + { + $builder->macro('onlyTrashed', function(Builder $builder) + { + $model = $builder->getModel(); + + $this->remove($builder, $model); + + $builder->getQuery()->whereNotNull($model->getQualifiedDeletedAtColumn()); + + return $builder; + }); + } + + /** + * Determine if the given where clause is a soft delete constraint. + * + * @param array $where + * @param string $column + * @return bool + */ + protected function isSoftDeleteConstraint(array $where, $column) + { + return $where['type'] == 'Null' && $where['column'] == $column; + } + +} diff --git a/Grammar.php b/Grammar.php index 32594bf03..e1e600691 100755 --- a/Grammar.php +++ b/Grammar.php @@ -30,16 +30,17 @@ public function wrapTable($table) { if ($this->isExpression($table)) return $this->getValue($table); - return $this->wrap($this->tablePrefix.$table); + return $this->wrap($this->tablePrefix.$table, true); } /** * Wrap a value in keyword identifiers. * * @param string $value + * @param bool $prefixAlias * @return string */ - public function wrap($value) + public function wrap($value, $prefixAlias = false) { if ($this->isExpression($value)) return $this->getValue($value); @@ -50,7 +51,9 @@ public function wrap($value) { $segments = explode(' ', $value); - return $this->wrap($segments[0]).' as '.$this->wrap($segments[2]); + if ($prefixAlias) $segments[2] = $this->tablePrefix.$segments[2]; + + return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[2]); } $wrapped = array(); @@ -83,7 +86,9 @@ public function wrap($value) */ protected function wrapValue($value) { - return $value !== '*' ? sprintf($this->wrapper, $value) : $value; + if ($value === '*') return $value; + + return '"'.str_replace('"', '""', $value).'"'; } /** @@ -165,7 +170,7 @@ public function getTablePrefix() * Set the grammar's table prefix. * * @param string $prefix - * @return \Illuminate\Database\Grammar + * @return $this */ public function setTablePrefix($prefix) { @@ -174,4 +179,4 @@ public function setTablePrefix($prefix) return $this; } -} \ No newline at end of file +} diff --git a/MigrationServiceProvider.php b/MigrationServiceProvider.php index 6915d9375..aeca8c52f 100755 --- a/MigrationServiceProvider.php +++ b/MigrationServiceProvider.php @@ -1,207 +1,226 @@ -registerRepository(); - - // Once we have registered the migrator instance we will go ahead and register - // all of the migration related commands that are used by the "Artisan" CLI - // so that they may be easily accessed for registering with the consoles. - $this->registerMigrator(); - - $this->registerCommands(); - } - - /** - * Register the migration repository service. - * - * @return void - */ - protected function registerRepository() - { - $this->app->bindShared('migration.repository', function($app) - { - $table = $app['config']['database.migrations']; - - return new DatabaseMigrationRepository($app['db'], $table); - }); - } - - /** - * Register the migrator service. - * - * @return void - */ - protected function registerMigrator() - { - // The migrator is responsible for actually running and rollback the migration - // files in the application. We'll pass in our database connection resolver - // so the migrator can resolve any of these connections when it needs to. - $this->app->bindShared('migrator', function($app) - { - $repository = $app['migration.repository']; - - return new Migrator($repository, $app['db'], $app['files']); - }); - } - - /** - * Register all of the migration commands. - * - * @return void - */ - protected function registerCommands() - { - $commands = array('Migrate', 'Rollback', 'Reset', 'Refresh', 'Install', 'Make'); - - // We'll simply spin through the list of commands that are migration related - // and register each one of them with an application container. They will - // be resolved in the Artisan start file and registered on the console. - foreach ($commands as $command) - { - $this->{'register'.$command.'Command'}(); - } - - // Once the commands are registered in the application IoC container we will - // register them with the Artisan start event so that these are available - // when the Artisan application actually starts up and is getting used. - $this->commands( - 'command.migrate', 'command.migrate.make', - 'command.migrate.install', 'command.migrate.rollback', - 'command.migrate.reset', 'command.migrate.refresh' - ); - } - - /** - * Register the "migrate" migration command. - * - * @return void - */ - protected function registerMigrateCommand() - { - $this->app->bindShared('command.migrate', function($app) - { - $packagePath = $app['path.base'].'/vendor'; - - return new MigrateCommand($app['migrator'], $packagePath); - }); - } - - /** - * Register the "rollback" migration command. - * - * @return void - */ - protected function registerRollbackCommand() - { - $this->app->bindShared('command.migrate.rollback', function($app) - { - return new RollbackCommand($app['migrator']); - }); - } - - /** - * Register the "reset" migration command. - * - * @return void - */ - protected function registerResetCommand() - { - $this->app->bindShared('command.migrate.reset', function($app) - { - return new ResetCommand($app['migrator']); - }); - } - - /** - * Register the "refresh" migration command. - * - * @return void - */ - protected function registerRefreshCommand() - { - $this->app->bindShared('command.migrate.refresh', function($app) - { - return new RefreshCommand; - }); - } - - /** - * Register the "install" migration command. - * - * @return void - */ - protected function registerInstallCommand() - { - $this->app->bindShared('command.migrate.install', function($app) - { - return new InstallCommand($app['migration.repository']); - }); - } - - /** - * Register the "install" migration command. - * - * @return void - */ - protected function registerMakeCommand() - { - $this->app->bindShared('migration.creator', function($app) - { - return new MigrationCreator($app['files']); - }); - - $this->app->bindShared('command.migrate.make', function($app) - { - // Once we have the migration creator registered, we will create the command - // and inject the creator. The creator is responsible for the actual file - // creation of the migrations, and may be extended by these developers. - $creator = $app['migration.creator']; - - $packagePath = $app['path.base'].'/vendor'; - - return new MakeCommand($creator, $packagePath); - }); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return array( - 'migrator', 'migration.repository', 'command.migrate', - 'command.migrate.rollback', 'command.migrate.reset', - 'command.migrate.refresh', 'command.migrate.install', - 'migration.creator', 'command.migrate.make', - ); - } - -} \ No newline at end of file +registerRepository(); + + // Once we have registered the migrator instance we will go ahead and register + // all of the migration related commands that are used by the "Artisan" CLI + // so that they may be easily accessed for registering with the consoles. + $this->registerMigrator(); + + $this->registerCommands(); + } + + /** + * Register the migration repository service. + * + * @return void + */ + protected function registerRepository() + { + $this->app->singleton('migration.repository', function($app) + { + $table = $app['config']['database.migrations']; + + return new DatabaseMigrationRepository($app['db'], $table); + }); + } + + /** + * Register the migrator service. + * + * @return void + */ + protected function registerMigrator() + { + // The migrator is responsible for actually running and rollback the migration + // files in the application. We'll pass in our database connection resolver + // so the migrator can resolve any of these connections when it needs to. + $this->app->singleton('migrator', function($app) + { + $repository = $app['migration.repository']; + + return new Migrator($repository, $app['db'], $app['files']); + }); + } + + /** + * Register all of the migration commands. + * + * @return void + */ + protected function registerCommands() + { + $commands = array('Migrate', 'Rollback', 'Reset', 'Refresh', 'Install', 'Make', 'Status'); + + // We'll simply spin through the list of commands that are migration related + // and register each one of them with an application container. They will + // be resolved in the Artisan start file and registered on the console. + foreach ($commands as $command) + { + $this->{'register'.$command.'Command'}(); + } + + // Once the commands are registered in the application IoC container we will + // register them with the Artisan start event so that these are available + // when the Artisan application actually starts up and is getting used. + $this->commands( + 'command.migrate', 'command.migrate.make', + 'command.migrate.install', 'command.migrate.rollback', + 'command.migrate.reset', 'command.migrate.refresh', + 'command.migrate.status' + ); + } + + /** + * Register the "migrate" migration command. + * + * @return void + */ + protected function registerMigrateCommand() + { + $this->app->singleton('command.migrate', function($app) + { + return new MigrateCommand($app['migrator']); + }); + } + + /** + * Register the "rollback" migration command. + * + * @return void + */ + protected function registerRollbackCommand() + { + $this->app->singleton('command.migrate.rollback', function($app) + { + return new RollbackCommand($app['migrator']); + }); + } + + /** + * Register the "reset" migration command. + * + * @return void + */ + protected function registerResetCommand() + { + $this->app->singleton('command.migrate.reset', function($app) + { + return new ResetCommand($app['migrator']); + }); + } + + /** + * Register the "refresh" migration command. + * + * @return void + */ + protected function registerRefreshCommand() + { + $this->app->singleton('command.migrate.refresh', function() + { + return new RefreshCommand; + }); + } + + protected function registerStatusCommand() + { + $this->app->singleton('command.migrate.status', function($app) + { + return new StatusCommand($app['migrator']); + }); + } + + /** + * Register the "install" migration command. + * + * @return void + */ + protected function registerInstallCommand() + { + $this->app->singleton('command.migrate.install', function($app) + { + return new InstallCommand($app['migration.repository']); + }); + } + + /** + * Register the "make" migration command. + * + * @return void + */ + protected function registerMakeCommand() + { + $this->registerCreator(); + + $this->app->singleton('command.migrate.make', function($app) + { + // Once we have the migration creator registered, we will create the command + // and inject the creator. The creator is responsible for the actual file + // creation of the migrations, and may be extended by these developers. + $creator = $app['migration.creator']; + + $composer = $app['composer']; + + return new MigrateMakeCommand($creator, $composer); + }); + } + + /** + * Register the migration creator. + * + * @return void + */ + protected function registerCreator() + { + $this->app->singleton('migration.creator', function($app) + { + return new MigrationCreator($app['files']); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return array( + 'migrator', 'migration.repository', 'command.migrate', + 'command.migrate.rollback', 'command.migrate.reset', + 'command.migrate.refresh', 'command.migrate.install', + 'command.migrate.status', 'migration.creator', + 'command.migrate.make', + ); + } + +} diff --git a/Migrations/DatabaseMigrationRepository.php b/Migrations/DatabaseMigrationRepository.php index d7104b791..88947100d 100755 --- a/Migrations/DatabaseMigrationRepository.php +++ b/Migrations/DatabaseMigrationRepository.php @@ -82,7 +82,7 @@ public function log($file, $batch) */ public function delete($migration) { - $query = $this->table()->where('migration', $migration->migration)->delete(); + $this->table()->where('migration', $migration->migration)->delete(); } /** @@ -178,4 +178,4 @@ public function setSource($name) $this->connection = $name; } -} \ No newline at end of file +} diff --git a/Migrations/Migration.php b/Migrations/Migration.php index 05a6b9402..eb75d1430 100755 --- a/Migrations/Migration.php +++ b/Migrations/Migration.php @@ -19,4 +19,4 @@ public function getConnection() return $this->connection; } -} \ No newline at end of file +} diff --git a/Migrations/MigrationCreator.php b/Migrations/MigrationCreator.php index daf24df05..e70b627b5 100755 --- a/Migrations/MigrationCreator.php +++ b/Migrations/MigrationCreator.php @@ -59,7 +59,8 @@ public function create($name, $path, $table = null, $create = false) * Get the migration stub file. * * @param string $table - * @return void + * @param bool $create + * @return string */ protected function getStub($table, $create) { @@ -89,7 +90,7 @@ protected function getStub($table, $create) */ protected function populateStub($name, $stub, $table) { - $stub = str_replace('{{class}}', studly_case($name), $stub); + $stub = str_replace('{{class}}', $this->getClassName($name), $stub); // Here we will replace the table place-holders with the table specified by // the developer, which is useful for quickly creating a tables creation @@ -102,6 +103,17 @@ protected function populateStub($name, $stub, $table) return $stub; } + /** + * Get the class name of a migration name. + * + * @param string $name + * @return string + */ + protected function getClassName($name) + { + return studly_case($name); + } + /** * Fire the registered post create hooks. * @@ -118,7 +130,7 @@ protected function firePostCreateHooks() /** * Register a post migration create hook. * - * @param Closure $callback + * @param \Closure $callback * @return void */ public function afterCreate(Closure $callback) @@ -168,4 +180,4 @@ public function getFilesystem() return $this->files; } -} \ No newline at end of file +} diff --git a/Migrations/Migrator.php b/Migrations/Migrator.php index 386532e51..fcb0e41bd 100755 --- a/Migrations/Migrator.php +++ b/Migrations/Migrator.php @@ -68,15 +68,17 @@ public function run($path, $pretend = false) { $this->notes = array(); - $this->requireFiles($path, $files = $this->getMigrationFiles($path)); + $files = $this->getMigrationFiles($path); // Once we grab all of the migration files for the path, we will compare them // against the migrations that have already been run for this package then - // run all of the oustanding migrations against the database connection. + // run each of the outstanding migrations against a database connection. $ran = $this->repository->getRan(); $migrations = array_diff($files, $ran); + $this->requireFiles($path, $migrations); + $this->runMigrationList($migrations, $pretend); } @@ -236,7 +238,8 @@ public function getMigrationFiles($path) /** * Require in all the migration files in a given path. * - * @param array $files + * @param string $path + * @param array $files * @return void */ public function requireFiles($path, array $files) @@ -248,6 +251,7 @@ public function requireFiles($path, array $files) * Pretend to run the migrations. * * @param object $migration + * @param string $method * @return void */ protected function pretendToRun($migration, $method) @@ -321,11 +325,12 @@ public function getNotes() /** * Resolve the database connection instance. * + * @param string $connection * @return \Illuminate\Database\Connection */ - public function resolveConnection() + public function resolveConnection($connection) { - return $this->resolver->connection($this->connection); + return $this->resolver->connection($connection); } /** @@ -376,4 +381,4 @@ public function getFilesystem() return $this->files; } -} \ No newline at end of file +} diff --git a/Migrations/stubs/blank.stub b/Migrations/stubs/blank.stub index 5ce40164c..a71120195 100755 --- a/Migrations/stubs/blank.stub +++ b/Migrations/stubs/blank.stub @@ -1,5 +1,6 @@ [], + 'join' => [], + 'where' => [], + 'having' => [], + 'order' => [], + ); /** * An aggregate function and column to be run. @@ -121,32 +131,39 @@ class Builder { public $unions; /** - * Indicates whether row locking is being used. + * The maximum number of union records to return. * - * @var string|bool + * @var int */ - public $lock; + public $unionLimit; /** - * The key that should be used when caching the query. + * The number of union records to skip. * - * @var string + * @var int */ - protected $cacheKey; + public $unionOffset; /** - * The number of minutes to cache the query. + * The orderings for the union query. * - * @var int + * @var array + */ + public $unionOrders; + + /** + * Indicates whether row locking is being used. + * + * @var string|bool */ - protected $cacheMinutes; + public $lock; /** - * The tags for the query cache. + * The field backups currently in use. * * @var array */ - protected $cacheTags; + protected $backups = []; /** * All of the available clause operators. @@ -157,8 +174,17 @@ class Builder { '=', '<', '>', '<=', '>=', '<>', '!=', 'like', 'not like', 'between', 'ilike', '&', '|', '^', '<<', '>>', + 'rlike', 'regexp', 'not regexp', + '~', '~*', '!~', '!~*', ); + /** + * Whether use write pdo for select. + * + * @var bool + */ + protected $useWritePdo = false; + /** * Create a new query builder instance. * @@ -180,7 +206,7 @@ public function __construct(ConnectionInterface $connection, * Set the columns to be selected. * * @param array $columns - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function select($columns = array('*')) { @@ -189,11 +215,64 @@ public function select($columns = array('*')) return $this; } + /** + * Add a new "raw" select expression to the query. + * + * @param string $expression + * @param array $bindings + * @return \Illuminate\Database\Query\Builder|static + */ + public function selectRaw($expression, array $bindings = array()) + { + $this->addSelect(new Expression($expression)); + + if ($bindings) + { + $this->addBinding($bindings, 'select'); + } + + return $this; + } + + /** + * Add a subselect expression to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @param string $as + * @return \Illuminate\Database\Query\Builder|static + */ + public function selectSub($query, $as) + { + if ($query instanceof Closure) + { + $callback = $query; + + $callback($query = $this->newQuery()); + } + + if ($query instanceof Builder) + { + $bindings = $query->getBindings(); + + $query = $query->toSql(); + } + elseif (is_string($query)) + { + $bindings = []; + } + else + { + throw new InvalidArgumentException; + } + + return $this->selectRaw('('.$query.') as '.$this->grammar->wrap($as), $bindings); + } + /** * Add a new select column to the query. * * @param mixed $column - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function addSelect($column) { @@ -207,7 +286,7 @@ public function addSelect($column) /** * Force the query to only return distinct results. * - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function distinct() { @@ -220,7 +299,7 @@ public function distinct() * Set the table which the query is targeting. * * @param string $table - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function from($table) { @@ -233,12 +312,12 @@ public function from($table) * Add a join clause to the query. * * @param string $table - * @param string $first + * @param string $one * @param string $operator * @param string $two * @param string $type - * @param bool $where - * @return \Illuminate\Database\Query\Builder|static + * @param bool $where + * @return $this */ public function join($table, $one, $operator = null, $two = null, $type = 'inner', $where = false) { @@ -247,7 +326,7 @@ public function join($table, $one, $operator = null, $two = null, $type = 'inner // one condition, so we'll add the join and call a Closure with the query. if ($one instanceof Closure) { - $this->joins[] = new JoinClause($this, $type, $table); + $this->joins[] = new JoinClause($type, $table); call_user_func($one, end($this->joins)); } @@ -257,7 +336,7 @@ public function join($table, $one, $operator = null, $two = null, $type = 'inner // this simple join clauses attached to it. There is not a join callback. else { - $join = new JoinClause($this, $type, $table); + $join = new JoinClause($type, $table); $this->joins[] = $join->on( $one, $operator, $two, 'and', $where @@ -271,7 +350,7 @@ public function join($table, $one, $operator = null, $two = null, $type = 'inner * Add a "join where" clause to the query. * * @param string $table - * @param string $first + * @param string $one * @param string $operator * @param string $two * @param string $type @@ -300,7 +379,7 @@ public function leftJoin($table, $first, $operator = null, $second = null) * Add a "join where" clause to the query. * * @param string $table - * @param string $first + * @param string $one * @param string $operator * @param string $two * @return \Illuminate\Database\Query\Builder|static @@ -310,6 +389,34 @@ public function leftJoinWhere($table, $one, $operator, $two) return $this->joinWhere($table, $one, $operator, $two, 'left'); } + /** + * Add a right join to the query. + * + * @param string $table + * @param string $first + * @param string $operator + * @param string $second + * @return \Illuminate\Database\Query\Builder|static + */ + public function rightJoin($table, $first, $operator = null, $second = null) + { + return $this->join($table, $first, $operator, $second, 'right'); + } + + /** + * Add a "right join where" clause to the query. + * + * @param string $table + * @param string $one + * @param string $operator + * @param string $two + * @return \Illuminate\Database\Query\Builder|static + */ + public function rightJoinWhere($table, $one, $operator, $two) + { + return $this->joinWhere($table, $one, $operator, $two, 'right'); + } + /** * Add a basic where clause to the query. * @@ -317,19 +424,36 @@ public function leftJoinWhere($table, $one, $operator, $two) * @param string $operator * @param mixed $value * @param string $boolean - * @return \Illuminate\Database\Query\Builder|static + * @return $this * * @throws \InvalidArgumentException */ public function where($column, $operator = null, $value = null, $boolean = 'and') { + // If the column is an array, we will assume it is an array of key-value pairs + // and can add them each as a where clause. We will maintain the boolean we + // received when the method was called and pass it into the nested where. + if (is_array($column)) + { + return $this->whereNested(function($query) use ($column) + { + foreach ($column as $key => $value) + { + $query->where($key, '=', $value); + } + }, $boolean); + } + + // Here we will make some assumptions about the operator. If only 2 values are + // passed to the method, we will assume that the operator is an equals sign + // and keep going. Otherwise, we'll require the operator to be passed in. if (func_num_args() == 2) { list($value, $operator) = array($operator, '='); } elseif ($this->invalidOperatorAndValue($operator, $value)) { - throw new \InvalidArgumentException("Value must be provided."); + throw new InvalidArgumentException("Value must be provided."); } // If the columns is actually a Closure instance, we will assume the developer @@ -373,7 +497,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' if ( ! $value instanceof Expression) { - $this->bindings[] = $value; + $this->addBinding($value, 'where'); } return $this; @@ -396,7 +520,7 @@ public function orWhere($column, $operator = null, $value = null) * Determine if the given operator and value combination is legal. * * @param string $operator - * @param mxied $value + * @param mixed $value * @return bool */ protected function invalidOperatorAndValue($operator, $value) @@ -412,7 +536,7 @@ protected function invalidOperatorAndValue($operator, $value) * @param string $sql * @param array $bindings * @param string $boolean - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function whereRaw($sql, array $bindings = array(), $boolean = 'and') { @@ -420,7 +544,7 @@ public function whereRaw($sql, array $bindings = array(), $boolean = 'and') $this->wheres[] = compact('type', 'sql', 'boolean'); - $this->bindings = array_merge($this->bindings, $bindings); + $this->addBinding($bindings, 'where'); return $this; } @@ -444,7 +568,7 @@ public function orWhereRaw($sql, array $bindings = array()) * @param array $values * @param string $boolean * @param bool $not - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function whereBetween($column, array $values, $boolean = 'and', $not = false) { @@ -452,7 +576,7 @@ public function whereBetween($column, array $values, $boolean = 'and', $not = fa $this->wheres[] = compact('column', 'type', 'boolean', 'not'); - $this->bindings = array_merge($this->bindings, $values); + $this->addBinding($values, 'where'); return $this; } @@ -462,10 +586,9 @@ public function whereBetween($column, array $values, $boolean = 'and', $not = fa * * @param string $column * @param array $values - * @param bool $not * @return \Illuminate\Database\Query\Builder|static */ - public function orWhereBetween($column, array $values, $not = false) + public function orWhereBetween($column, array $values) { return $this->whereBetween($column, $values, 'or'); } @@ -507,19 +630,28 @@ public function whereNested(Closure $callback, $boolean = 'and') // To handle nested queries we'll actually create a brand new query instance // and pass it off to the Closure that we have. The Closure can simply do // do whatever it wants to a query then we will store it for compiling. - $type = 'Nested'; - $query = $this->newQuery(); $query->from($this->from); call_user_func($callback, $query); - // Once we have let the Closure do its things, we can gather the bindings on - // the nested query builder and merge them into these bindings since they - // need to get extracted out of the children and assigned to the array. + return $this->addNestedWhereQuery($query, $boolean); + } + + /** + * Add another query builder as a nested where to the query builder. + * + * @param \Illuminate\Database\Query\Builder|static $query + * @param string $boolean + * @return $this + */ + public function addNestedWhereQuery($query, $boolean = 'and') + { if (count($query->wheres)) { + $type = 'Nested'; + $this->wheres[] = compact('type', 'query', 'boolean'); $this->mergeBindings($query); @@ -535,7 +667,7 @@ public function whereNested(Closure $callback, $boolean = 'and') * @param string $operator * @param \Closure $callback * @param string $boolean - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ protected function whereSub($column, $operator, Closure $callback, $boolean) { @@ -561,7 +693,7 @@ protected function whereSub($column, $operator, Closure $callback, $boolean) * @param \Closure $callback * @param string $boolean * @param bool $not - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function whereExists(Closure $callback, $boolean = 'and', $not = false) { @@ -623,7 +755,7 @@ public function orWhereNotExists(Closure $callback) * @param mixed $values * @param string $boolean * @param bool $not - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function whereIn($column, $values, $boolean = 'and', $not = false) { @@ -639,7 +771,7 @@ public function whereIn($column, $values, $boolean = 'and', $not = false) $this->wheres[] = compact('type', 'column', 'values', 'boolean'); - $this->bindings = array_merge($this->bindings, $values); + $this->addBinding($values, 'where'); return $this; } @@ -688,7 +820,7 @@ public function orWhereNotIn($column, $values) * @param \Closure $callback * @param string $boolean * @param bool $not - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ protected function whereInSub($column, Closure $callback, $boolean, $not) { @@ -712,7 +844,7 @@ protected function whereInSub($column, Closure $callback, $boolean, $not) * @param string $column * @param string $boolean * @param bool $not - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function whereNull($column, $boolean = 'and', $not = false) { @@ -757,12 +889,87 @@ public function orWhereNotNull($column) return $this->whereNotNull($column, 'or'); } + /** + * Add a "where date" statement to the query. + * + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static + */ + public function whereDate($column, $operator, $value, $boolean = 'and') + { + return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); + } + + /** + * Add a "where day" statement to the query. + * + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static + */ + public function whereDay($column, $operator, $value, $boolean = 'and') + { + return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean); + } + + /** + * Add a "where month" statement to the query. + * + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static + */ + public function whereMonth($column, $operator, $value, $boolean = 'and') + { + return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean); + } + + /** + * Add a "where year" statement to the query. + * + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder|static + */ + public function whereYear($column, $operator, $value, $boolean = 'and') + { + return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean); + } + + /** + * Add a date based (year, month, day) statement to the query. + * + * @param string $type + * @param string $column + * @param string $operator + * @param int $value + * @param string $boolean + * @return $this + */ + protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') + { + $this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value'); + + $this->addBinding($value, 'where'); + + return $this; + } + /** * Handles dynamic "where" clauses to the query. * * @param string $method * @param string $parameters - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function dynamicWhere($method, $parameters) { @@ -823,12 +1030,15 @@ protected function addDynamic($segment, $connector, $parameters, $index) /** * Add a "group by" clause to the query. * - * @param dynamic $columns - * @return \Illuminate\Database\Query\Builder|static + * @param array|string $column,... + * @return $this */ public function groupBy() { - $this->groups = array_merge((array) $this->groups, func_get_args()); + foreach (func_get_args() as $arg) + { + $this->groups = array_merge((array) $this->groups, is_array($arg) ? $arg : [$arg]); + } return $this; } @@ -839,26 +1049,40 @@ public function groupBy() * @param string $column * @param string $operator * @param string $value - * @return \Illuminate\Database\Query\Builder|static + * @param string $boolean + * @return $this */ - public function having($column, $operator = null, $value = null) + public function having($column, $operator = null, $value = null, $boolean = 'and') { $type = 'basic'; - $this->havings[] = compact('type', 'column', 'operator', 'value'); + $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); - $this->bindings[] = $value; + $this->addBinding($value, 'having'); return $this; } + /** + * Add a "or having" clause to the query. + * + * @param string $column + * @param string $operator + * @param string $value + * @return \Illuminate\Database\Query\Builder|static + */ + public function orHaving($column, $operator = null, $value = null) + { + return $this->having($column, $operator, $value, 'or'); + } + /** * Add a raw having clause to the query. * * @param string $sql * @param array $bindings * @param string $boolean - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function havingRaw($sql, array $bindings = array(), $boolean = 'and') { @@ -866,7 +1090,7 @@ public function havingRaw($sql, array $bindings = array(), $boolean = 'and') $this->havings[] = compact('type', 'sql', 'boolean'); - $this->bindings = array_merge($this->bindings, $bindings); + $this->addBinding($bindings, 'having'); return $this; } @@ -888,13 +1112,14 @@ public function orHavingRaw($sql, array $bindings = array()) * * @param string $column * @param string $direction - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function orderBy($column, $direction = 'asc') { + $property = $this->unions ? 'unionOrders' : 'orders'; $direction = strtolower($direction) == 'asc' ? 'asc' : 'desc'; - $this->orders[] = compact('column', 'direction'); + $this->{$property}[] = compact('column', 'direction'); return $this; } @@ -921,12 +1146,12 @@ public function oldest($column = 'created_at') return $this->orderBy($column, 'asc'); } - /* + /** * Add a raw "order by" clause to the query. * * @param string $sql * @param array $bindings - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function orderByRaw($sql, $bindings = array()) { @@ -934,7 +1159,7 @@ public function orderByRaw($sql, $bindings = array()) $this->orders[] = compact('type', 'sql'); - $this->bindings = array_merge($this->bindings, $bindings); + $this->addBinding($bindings, 'order'); return $this; } @@ -943,11 +1168,13 @@ public function orderByRaw($sql, $bindings = array()) * Set the "offset" value of the query. * * @param int $value - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function offset($value) { - $this->offset = $value; + $property = $this->unions ? 'unionOffset' : 'offset'; + + $this->$property = max(0, $value); return $this; } @@ -967,11 +1194,13 @@ public function skip($value) * Set the "limit" value of the query. * * @param int $value - * @return \Illuminate\Database\Query\Builder|static + * @return $this */ public function limit($value) { - if ($value > 0) $this->limit = $value; + $property = $this->unions ? 'unionLimit' : 'limit'; + + if ($value > 0) $this->$property = $value; return $this; } @@ -1003,7 +1232,7 @@ public function forPage($page, $perPage = 15) * Add a union statement to the query. * * @param \Illuminate\Database\Query\Builder|\Closure $query - * @param bool $all + * @param bool $all * @return \Illuminate\Database\Query\Builder|static */ public function union($query, $all = false) @@ -1032,8 +1261,8 @@ public function unionAll($query) /** * Lock the selected rows in the table. * - * @param bool $update - * @return \Illuminate\Database\Query\Builder + * @param bool $value + * @return $this */ public function lock($value = true) { @@ -1072,46 +1301,6 @@ public function toSql() return $this->grammar->compileSelect($this); } - /** - * Indicate that the query results should be cached. - * - * @param \Carbon\Carbon|\Datetime|int $minutes - * @param string $key - * @return \Illuminate\Database\Query\Builder|static - */ - public function remember($minutes, $key = null) - { - list($this->cacheMinutes, $this->cacheKey) = array($minutes, $key); - - return $this; - } - - /** - * Indicate that the query results should be cached forever. - * - * @param string $key - * @return \Illuminate\Database\Query\Builder|static - */ - public function rememberForever($key = null) - { - list($this->cacheMinutes, $this->cacheKey) = array(-1, $key); - - return $this; - } - - /** - * Indicate that the results, if cached, should use the given cache tags. - * - * @param array|dynamic $cacheTags - * @return \Illuminate\Database\Query\Builder|static - */ - public function cacheTags($cacheTags) - { - $this->cacheTags = $cacheTags; - - return $this; - } - /** * Execute a query for a single record by ID. * @@ -1158,8 +1347,6 @@ public function first($columns = array('*')) */ public function get($columns = array('*')) { - if ( ! is_null($this->cacheMinutes)) return $this->getCached($columns); - return $this->getFresh($columns); } @@ -1183,96 +1370,98 @@ public function getFresh($columns = array('*')) */ protected function runSelect() { - return $this->connection->select($this->toSql(), $this->bindings); + if ($this->useWritePdo) + { + return $this->connection->select($this->toSql(), $this->getBindings(), false); + } + + return $this->connection->select($this->toSql(), $this->getBindings()); } /** - * Execute the query as a cached "select" statement. + * Paginate the given query into a simple paginator. * + * @param int $perPage * @param array $columns - * @return array + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - public function getCached($columns = array('*')) + public function paginate($perPage = 15, $columns = ['*']) { - if (is_null($this->columns)) $this->columns = $columns; - - // If the query is requested ot be cached, we will cache it using a unique key - // for this database connection and query statement, including the bindings - // that are used on this query, providing great convenience when caching. - list($key, $minutes) = $this->getCacheInfo(); + $page = Paginator::resolveCurrentPage(); - $cache = $this->getCache(); + $total = $this->getCountForPagination(); - $callback = $this->getCacheCallback($columns); + $results = $this->forPage($page, $perPage)->get($columns); - // If the "minutes" value is less than zero, we will use that as the indicator - // that the value should be remembered values should be stored indefinitely - // and if we have minutes we will use the typical remember function here. - if ($minutes < 0) - { - return $cache->rememberForever($key, $callback); - } - else - { - return $cache->remember($key, $minutes, $callback); - } + return new LengthAwarePaginator($results, $total, $perPage, $page, [ + 'path' => Paginator::resolveCurrentPath() + ]); } /** - * Get the cache object with tags assigned, if applicable. + * Get a paginator only supporting simple next and previous links. * - * @return \Illuminate\Cache\CacheManager + * This is more efficient on larger data-sets, etc. + * + * @param int $perPage + * @param array $columns + * @return \Illuminate\Contracts\Pagination\Paginator */ - protected function getCache() + public function simplePaginate($perPage = 15, $columns = ['*']) { - $cache = $this->connection->getCacheManager(); + $page = Paginator::resolveCurrentPage(); - return $this->cacheTags ? $cache->tags($this->cacheTags) : $cache; - } + $this->skip(($page - 1) * $perPage)->take($perPage + 1); - /** - * Get the cache key and cache minutes as an array. - * - * @return array - */ - protected function getCacheInfo() - { - return array($this->getCacheKey(), $this->cacheMinutes); + return new Paginator($this->get($columns), $page, $perPage, [ + 'path' => Paginator::resolveCurrentPath() + ]); } /** - * Get a unique cache key for the complete query. + * Get the count of the total records for the paginator. * - * @return string + * @return int */ - public function getCacheKey() + public function getCountForPagination() { - return $this->cacheKey ?: $this->generateCacheKey(); + $this->backupFieldsForCount(); + + $total = $this->count(); + + $this->restoreFieldsForCount(); + + return $total; } /** - * Generate the unique cache key for the query. + * Backup some fields for the pagination count. * - * @return string + * @return void */ - public function generateCacheKey() + protected function backupFieldsForCount() { - $name = $this->connection->getName(); + foreach (['orders', 'limit', 'offset'] as $field) + { + $this->backups[$field] = $this->{$field}; - return md5($name.$this->toSql().serialize($this->bindings)); + $this->{$field} = null; + } } /** - * Get the Closure callback used when caching queries. + * Restore some fields after the pagination count. * - * @param array $columns - * @return \Closure + * @return void */ - protected function getCacheCallback($columns) + protected function restoreFieldsForCount() { - $me = $this; + foreach (['orders', 'limit', 'offset'] as $field) + { + $this->{$field} = $this->backups[$field]; + } - return function() use ($me, $columns) { return $me->getFresh($columns); }; + $this->backups = []; } /** @@ -1282,7 +1471,7 @@ protected function getCacheCallback($columns) * @param callable $callback * @return void */ - public function chunk($count, $callback) + public function chunk($count, callable $callback) { $results = $this->forPage($page = 1, $count)->get(); @@ -1291,7 +1480,10 @@ public function chunk($count, $callback) // On each chunk result set, we will pass them to the callback and then let the // developer take care of everything within the callback, which allows us to // keep the memory low for spinning through large result sets for working. - call_user_func($callback, $results); + if (call_user_func($callback, $results) === false) + { + break; + } $page++; @@ -1310,24 +1502,9 @@ public function lists($column, $key = null) { $columns = $this->getListSelect($column, $key); - // First we will just get all of the column values for the record result set - // then we can associate those values with the column if it was specified - // otherwise we can just give these values back without a specific key. $results = new Collection($this->get($columns)); - $values = $results->fetch($columns[0])->all(); - - // If a key was specified and we have results, we will go ahead and combine - // the values with the keys of all of the records so that the values can - // be accessed by the key of the rows instead of simply being numeric. - if ( ! is_null($key) && count($results) > 0) - { - $keys = $results->fetch($key)->all(); - - return array_combine($keys, $values); - } - - return $values; + return $results->lists($columns[0], array_get($columns, 1)); } /** @@ -1344,12 +1521,12 @@ protected function getListSelect($column, $key) // If the selected column contains a "dot", we will remove it so that the list // operation can run normally. Specifying the table is not needed, since we // really want the names of the columns as it is in this resulting array. - if (($dot = strpos($select[0], '.')) !== false) + return array_map(function($column) { - $select[0] = substr($select[0], $dot + 1); - } + $dot = strpos($column, '.'); - return $select; + return $dot === false ? $column : substr($column, $dot + 1); + }, $select); } /** @@ -1366,110 +1543,6 @@ public function implode($column, $glue = null) return implode($glue, $this->lists($column)); } - /** - * Get a paginator for the "select" statement. - * - * @param int $perPage - * @param array $columns - * @return \Illuminate\Pagination\Paginator - */ - public function paginate($perPage = 15, $columns = array('*')) - { - $paginator = $this->connection->getPaginator(); - - if (isset($this->groups)) - { - return $this->groupedPaginate($paginator, $perPage, $columns); - } - else - { - return $this->ungroupedPaginate($paginator, $perPage, $columns); - } - } - - /** - * Create a paginator for a grouped pagination statement. - * - * @param \Illuminate\Pagination\Environment $paginator - * @param int $perPage - * @param array $columns - * @return \Illuminate\Pagination\Paginator - */ - protected function groupedPaginate($paginator, $perPage, $columns) - { - $results = $this->get($columns); - - return $this->buildRawPaginator($paginator, $results, $perPage); - } - - /** - * Build a paginator instance from a raw result array. - * - * @param \Illuminate\Pagination\Environment $paginator - * @param array $results - * @param int $perPage - * @return \Illuminate\Pagination\Paginator - */ - public function buildRawPaginator($paginator, $results, $perPage) - { - // For queries which have a group by, we will actually retrieve the entire set - // of rows from the table and "slice" them via PHP. This is inefficient and - // the developer must be aware of this behavior; however, it's an option. - $start = ($paginator->getCurrentPage() - 1) * $perPage; - - $sliced = array_slice($results, $start, $perPage); - - return $paginator->make($sliced, count($results), $perPage); - } - - /** - * Create a paginator for an un-grouped pagination statement. - * - * @param \Illuminate\Pagination\Environment $paginator - * @param int $perPage - * @param array $columns - * @return \Illuminate\Pagination\Paginator - */ - protected function ungroupedPaginate($paginator, $perPage, $columns) - { - $total = $this->getPaginationCount(); - - // Once we have the total number of records to be paginated, we can grab the - // current page and the result array. Then we are ready to create a brand - // new Paginator instances for the results which will create the links. - $page = $paginator->getCurrentPage($total); - - $results = $this->forPage($page, $perPage)->get($columns); - - return $paginator->make($results, $total, $perPage); - } - - /** - * Get the count of the total records for pagination. - * - * @return int - */ - public function getPaginationCount() - { - list($orders, $this->orders) = array($this->orders, null); - - $columns = $this->columns; - - // Because some database engines may throw errors if we leave the ordering - // statements on the query, we will "back them up" and remove them from - // the query. Once we have the count we will put them back onto this. - $total = $this->count(); - - $this->orders = $orders; - - // Once the query is run we need to put the old select columns back on the - // instance so that the select query will run properly. Otherwise, they - // will be cleared, then the query will fire with all of the columns. - $this->columns = $columns; - - return $total; - } - /** * Determine if any rows exist for the current query. * @@ -1477,18 +1550,29 @@ public function getPaginationCount() */ public function exists() { - return $this->count() > 0; + $limit = $this->limit; + + $result = $this->limit(1)->count() > 0; + + $this->limit($limit); + + return $result; } /** * Retrieve the "count" result of the query. * - * @param string $column + * @param string $columns * @return int */ - public function count($column = '*') + public function count($columns = '*') { - return $this->aggregate(__FUNCTION__, array($column)); + if ( ! is_array($columns)) + { + $columns = array($columns); + } + + return (int) $this->aggregate(__FUNCTION__, $columns); } /** @@ -1521,7 +1605,9 @@ public function max($column) */ public function sum($column) { - return $this->aggregate(__FUNCTION__, array($column)); + $result = $this->aggregate(__FUNCTION__, array($column)); + + return $result ?: 0; } /** @@ -1546,16 +1632,20 @@ public function aggregate($function, $columns = array('*')) { $this->aggregate = compact('function', 'columns'); + $previousColumns = $this->columns; + $results = $this->get($columns); // Once we have executed the query, we will reset the aggregate property so // that more select queries can be executed against the database without // the aggregate value getting in the way when the grammar builds it. - $this->columns = null; $this->aggregate = null; + $this->aggregate = null; + + $this->columns = $previousColumns; if (isset($results[0])) { - $result = (array) $results[0]; + $result = array_change_key_case((array) $results[0]); return $result['aggregate']; } @@ -1595,7 +1685,10 @@ public function insert(array $values) foreach ($values as $record) { - $bindings = array_merge($bindings, array_values($record)); + foreach ($record as $value) + { + $bindings[] = $value; + } } $sql = $this->grammar->compileInsert($this, $values); @@ -1632,7 +1725,7 @@ public function insertGetId(array $values, $sequence = null) */ public function update(array $values) { - $bindings = array_values(array_merge($values, $this->bindings)); + $bindings = array_values(array_merge($values, $this->getBindings())); $sql = $this->grammar->compileUpdate($this, $values); @@ -1688,7 +1781,7 @@ public function delete($id = null) $sql = $this->grammar->compileDelete($this); - return $this->connection->delete($sql, $this->bindings); + return $this->connection->delete($sql, $this->getBindings()); } /** @@ -1725,7 +1818,7 @@ public function mergeWheres($wheres, $bindings) { $this->wheres = array_merge((array) $this->wheres, (array) $wheres); - $this->bindings = array_values(array_merge($this->bindings, (array) $bindings)); + $this->bindings['where'] = array_values(array_merge($this->bindings['where'], (array) $bindings)); } /** @@ -1754,11 +1847,21 @@ public function raw($value) } /** - * Get the current query value bindings. + * Get the current query value bindings in a flattened array. * * @return array */ public function getBindings() + { + return array_flatten($this->bindings); + } + + /** + * Get the raw array of bindings. + * + * @return array + */ + public function getRawBindings() { return $this->bindings; } @@ -1766,12 +1869,20 @@ public function getBindings() /** * Set the bindings on the query builder. * - * @param array $bindings - * @return \Illuminate\Database\Query\Builder + * @param array $bindings + * @param string $type + * @return $this + * + * @throws \InvalidArgumentException */ - public function setBindings(array $bindings) + public function setBindings(array $bindings, $type = 'where') { - $this->bindings = $bindings; + if ( ! array_key_exists($type, $this->bindings)) + { + throw new InvalidArgumentException("Invalid binding type: {$type}."); + } + + $this->bindings[$type] = $bindings; return $this; } @@ -1779,12 +1890,27 @@ public function setBindings(array $bindings) /** * Add a binding to the query. * - * @param mixed $value - * @return \Illuminate\Database\Query\Builder + * @param mixed $value + * @param string $type + * @return $this + * + * @throws \InvalidArgumentException */ - public function addBinding($value) + public function addBinding($value, $type = 'where') { - $this->bindings[] = $value; + if ( ! array_key_exists($type, $this->bindings)) + { + throw new InvalidArgumentException("Invalid binding type: {$type}."); + } + + if (is_array($value)) + { + $this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value)); + } + else + { + $this->bindings[$type][] = $value; + } return $this; } @@ -1793,11 +1919,11 @@ public function addBinding($value) * Merge an array of bindings into our bindings. * * @param \Illuminate\Database\Query\Builder $query - * @return \Illuminate\Database\Query\Builder + * @return $this */ public function mergeBindings(Builder $query) { - $this->bindings = array_values(array_merge($this->bindings, $query->bindings)); + $this->bindings = array_merge_recursive($this->bindings, $query->bindings); return $this; } @@ -1832,6 +1958,18 @@ public function getGrammar() return $this->grammar; } + /** + * Use the write pdo for query. + * + * @return $this + */ + public function useWritePdo() + { + $this->useWritePdo = true; + + return $this; + } + /** * Handle dynamic method calls into the method. * @@ -1850,7 +1988,7 @@ public function __call($method, $parameters) $className = get_class($this); - throw new \BadMethodCallException("Call to undefined method {$className}::{$method}()"); + throw new BadMethodCallException("Call to undefined method {$className}::{$method}()"); } } diff --git a/Query/Expression.php b/Query/Expression.php index 82cdba6b8..68d223656 100755 --- a/Query/Expression.php +++ b/Query/Expression.php @@ -40,4 +40,4 @@ public function __toString() return (string) $this->getValue(); } -} \ No newline at end of file +} diff --git a/Query/Grammars/Grammar.php b/Query/Grammars/Grammar.php index e5e2cb688..6998370bc 100755 --- a/Query/Grammars/Grammar.php +++ b/Query/Grammars/Grammar.php @@ -5,13 +5,6 @@ class Grammar extends BaseGrammar { - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '"%s"'; - /** * The components that make up a select clause. * @@ -135,6 +128,8 @@ protected function compileJoins(Builder $query, $joins) { $sql = array(); + $query->setBindings(array(), 'join'); + foreach ($joins as $join) { $table = $this->wrapTable($join->table); @@ -149,6 +144,11 @@ protected function compileJoins(Builder $query, $joins) $clauses[] = $this->compileJoinConstraint($clause); } + foreach ($join->bindings as $binding) + { + $query->addBinding($binding, 'join'); + } + // Once we have constructed the clauses, we'll need to take the boolean connector // off of the first clause as it obviously will not be required on that clause // because it leads the rest of the clauses, thus not requiring any boolean. @@ -306,6 +306,8 @@ protected function whereNotExists(Builder $query, $where) */ protected function whereIn(Builder $query, $where) { + if (empty($where['values'])) return '0 = 1'; + $values = $this->parameterize($where['values']); return $this->wrap($where['column']).' in ('.$values.')'; @@ -320,6 +322,8 @@ protected function whereIn(Builder $query, $where) */ protected function whereNotIn(Builder $query, $where) { + if (empty($where['values'])) return '1 = 1'; + $values = $this->parameterize($where['values']); return $this->wrap($where['column']).' not in ('.$values.')'; @@ -377,6 +381,69 @@ protected function whereNotNull(Builder $query, $where) return $this->wrap($where['column']).' is not null'; } + /** + * Compile a "where date" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereDate(Builder $query, $where) + { + return $this->dateBasedWhere('date', $query, $where); + } + + /** + * Compile a "where day" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereDay(Builder $query, $where) + { + return $this->dateBasedWhere('day', $query, $where); + } + + /** + * Compile a "where month" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereMonth(Builder $query, $where) + { + return $this->dateBasedWhere('month', $query, $where); + } + + /** + * Compile a "where year" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereYear(Builder $query, $where) + { + return $this->dateBasedWhere('year', $query, $where); + } + + /** + * Compile a date based where clause. + * + * @param string $type + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function dateBasedWhere($type, Builder $query, $where) + { + $value = $this->parameter($where['value']); + + return $type.'('.$this->wrap($where['column']).') '.$where['operator'].' '.$value; + } + /** * Compile a raw where clause. * @@ -410,11 +477,9 @@ protected function compileGroups(Builder $query, $groups) */ protected function compileHavings(Builder $query, $havings) { - $me = $this; - $sql = implode(' ', array_map(array($this, 'compileHaving'), $havings)); - return 'having '.preg_replace('/and /', '', $sql, 1); + return 'having '.preg_replace('/and |or /', '', $sql, 1); } /** @@ -448,7 +513,7 @@ protected function compileBasicHaving($having) $parameter = $this->parameter($having['value']); - return 'and '.$column.' '.$having['operator'].' '.$parameter; + return $having['boolean'].' '.$column.' '.$having['operator'].' '.$parameter; } /** @@ -460,13 +525,11 @@ protected function compileBasicHaving($having) */ protected function compileOrders(Builder $query, $orders) { - $me = $this; - - return 'order by '.implode(', ', array_map(function($order) use ($me) + return 'order by '.implode(', ', array_map(function($order) { if (isset($order['sql'])) return $order['sql']; - return $me->wrap($order['column']).' '.$order['direction']; + return $this->wrap($order['column']).' '.$order['direction']; } , $orders)); } @@ -510,6 +573,21 @@ protected function compileUnions(Builder $query) $sql .= $this->compileUnion($union); } + if (isset($query->unionOrders)) + { + $sql .= ' '.$this->compileOrders($query, $query->unionOrders); + } + + if (isset($query->unionLimit)) + { + $sql .= ' '.$this->compileLimit($query, $query->unionLimit); + } + + if (isset($query->unionOffset)) + { + $sql .= ' '.$this->compileOffset($query, $query->unionOffset); + } + return ltrim($sql); } @@ -619,7 +697,6 @@ public function compileUpdate(Builder $query, $values) * Compile a delete statement into SQL. * * @param \Illuminate\Database\Query\Builder $query - * @param array $values * @return string */ public function compileDelete(Builder $query) @@ -679,4 +756,4 @@ protected function removeLeadingBoolean($value) return preg_replace('/and |or /', '', $value, 1); } -} \ No newline at end of file +} diff --git a/Query/Grammars/MySqlGrammar.php b/Query/Grammars/MySqlGrammar.php index c801cf84d..b068e2b0d 100755 --- a/Query/Grammars/MySqlGrammar.php +++ b/Query/Grammars/MySqlGrammar.php @@ -4,13 +4,6 @@ class MySqlGrammar extends Grammar { - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '`%s`'; - /** * The components that make up a select clause. * @@ -99,4 +92,39 @@ public function compileUpdate(Builder $query, $values) return rtrim($sql); } -} \ No newline at end of file + /** + * Compile a delete statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + public function compileDelete(Builder $query) + { + $table = $this->wrapTable($query->from); + + $where = is_array($query->wheres) ? $this->compileWheres($query) : ''; + + if (isset($query->joins)) + { + $joins = ' '.$this->compileJoins($query, $query->joins); + + return trim("delete $table from {$table}{$joins} $where"); + } + + return trim("delete from $table $where"); + } + + /** + * Wrap a single string in keyword identifiers. + * + * @param string $value + * @return string + */ + protected function wrapValue($value) + { + if ($value === '*') return $value; + + return '`'.str_replace('`', '``', $value).'`'; + } + +} diff --git a/Query/Grammars/PostgresGrammar.php b/Query/Grammars/PostgresGrammar.php index 8c5aac0b1..7a8df9c4c 100755 --- a/Query/Grammars/PostgresGrammar.php +++ b/Query/Grammars/PostgresGrammar.php @@ -117,10 +117,8 @@ protected function compileUpdateWheres(Builder $query) { return 'where '.$this->removeLeadingBoolean($joinWhere); } - else - { - return $baseWhere.' '.$joinWhere; - } + + return $baseWhere.' '.$joinWhere; } /** @@ -173,4 +171,4 @@ public function compileTruncate(Builder $query) return array('truncate '.$this->wrapTable($query->from).' restart identity' => array()); } -} \ No newline at end of file +} diff --git a/Query/Grammars/SQLiteGrammar.php b/Query/Grammars/SQLiteGrammar.php index aac1ad1ec..01558d35c 100755 --- a/Query/Grammars/SQLiteGrammar.php +++ b/Query/Grammars/SQLiteGrammar.php @@ -74,4 +74,57 @@ public function compileTruncate(Builder $query) return $sql; } + /** + * Compile a "where day" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereDay(Builder $query, $where) + { + return $this->dateBasedWhere('%d', $query, $where); + } + + /** + * Compile a "where month" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereMonth(Builder $query, $where) + { + return $this->dateBasedWhere('%m', $query, $where); + } + + /** + * Compile a "where year" clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereYear(Builder $query, $where) + { + return $this->dateBasedWhere('%Y', $query, $where); + } + + /** + * Compile a date based where clause. + * + * @param string $type + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function dateBasedWhere($type, Builder $query, $where) + { + $value = str_pad($where['value'], 2, '0', STR_PAD_LEFT); + + $value = $this->parameter($value); + + return 'strftime(\''.$type.'\', '.$this->wrap($where['column']).') '.$where['operator'].' '.$value; + } + } diff --git a/Query/Grammars/SqlServerGrammar.php b/Query/Grammars/SqlServerGrammar.php index deda0b69c..24fd428ec 100755 --- a/Query/Grammars/SqlServerGrammar.php +++ b/Query/Grammars/SqlServerGrammar.php @@ -15,13 +15,6 @@ class SqlServerGrammar extends Grammar { '&', '&=', '|', '|=', '^', '^=', ); - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '[%s]'; - /** * Compile a select query into SQL. * @@ -59,7 +52,7 @@ protected function compileColumns(Builder $query, $columns) // If there is a limit on the query, but not an offset, we will add the top // clause to the query, which serves as a "limit" type clause within the // SQL Server system similar to the limit keywords available in MySQL. - if ($query->limit > 0 and $query->offset <= 0) + if ($query->limit > 0 && $query->offset <= 0) { $select .= 'top '.$query->limit.' '; } @@ -84,10 +77,8 @@ protected function compileFrom(Builder $query, $table) { return $from.' with(rowlock,'.($query->lock ? 'updlock,' : '').'holdlock)'; } - else - { - return $from; - } + + return $from; } /** @@ -119,8 +110,6 @@ protected function compileAnsiOffset(Builder $query, $components) // Next we need to calculate the constraints that should be placed on the query // to get the right offset and limit from our query but if there is no limit // set we will just handle the offset only since that is all that matters. - $start = $query->offset + 1; - $constraint = $this->compileRowConstraint($query); $sql = $this->concatenate($components); @@ -219,4 +208,17 @@ public function getDateFormat() return 'Y-m-d H:i:s.000'; } -} \ No newline at end of file + /** + * Wrap a single string in keyword identifiers. + * + * @param string $value + * @return string + */ + protected function wrapValue($value) + { + if ($value === '*') return $value; + + return '['.str_replace(']', ']]', $value).']'; + } + +} diff --git a/Query/JoinClause.php b/Query/JoinClause.php index 662bea7bf..cea79ed4c 100755 --- a/Query/JoinClause.php +++ b/Query/JoinClause.php @@ -2,13 +2,6 @@ class JoinClause { - /** - * The query builder instance. - * - * @var \Illuminate\Database\Query\Builder - */ - public $query; - /** * The type of join being performed. * @@ -30,18 +23,23 @@ class JoinClause { */ public $clauses = array(); + /** + * The "on" bindings for the join. + * + * @var array + */ + public $bindings = array(); + /** * Create a new join clause instance. * - * @param \Illuminate\Database\Query\Builder $query * @param string $type * @param string $table * @return void */ - public function __construct(Builder $query, $type, $table) + public function __construct($type, $table) { $this->type = $type; - $this->query = $query; $this->table = $table; } @@ -53,13 +51,13 @@ public function __construct(Builder $query, $type, $table) * @param string $second * @param string $boolean * @param bool $where - * @return \Illuminate\Database\Query\JoinClause + * @return $this */ public function on($first, $operator, $second, $boolean = 'and', $where = false) { $this->clauses[] = compact('first', 'operator', 'second', 'boolean', 'where'); - if ($where) $this->query->addBinding($second); + if ($where) $this->bindings[] = $second; return $this; } @@ -97,7 +95,6 @@ public function where($first, $operator, $second, $boolean = 'and') * @param string $first * @param string $operator * @param string $second - * @param string $boolean * @return \Illuminate\Database\Query\JoinClause */ public function orWhere($first, $operator, $second) @@ -105,4 +102,16 @@ public function orWhere($first, $operator, $second) return $this->on($first, $operator, $second, 'or', true); } -} \ No newline at end of file + /** + * Add an "on where is null" clause to the join + * + * @param string $column + * @param string $boolean + * @return \Illuminate\Database\Query\JoinClause + */ + public function whereNull($column, $boolean = 'and') + { + return $this->on($column, 'is', new Expression('null'), $boolean, false); + } + +} diff --git a/Query/Processors/MySqlProcessor.php b/Query/Processors/MySqlProcessor.php index 990810866..f77b41db2 100644 --- a/Query/Processors/MySqlProcessor.php +++ b/Query/Processors/MySqlProcessor.php @@ -1,7 +1,5 @@ column_name; }, $results); + return array_map(function($r) { $r = (object) $r; return $r->column_name; }, $results); } } diff --git a/Query/Processors/PostgresProcessor.php b/Query/Processors/PostgresProcessor.php index 10f39e7b4..665379df4 100755 --- a/Query/Processors/PostgresProcessor.php +++ b/Query/Processors/PostgresProcessor.php @@ -15,13 +15,15 @@ class PostgresProcessor extends Processor { */ public function processInsertGetId(Builder $query, $sql, $values, $sequence = null) { - $results = $query->getConnection()->select($sql, $values); + $results = $query->getConnection()->selectFromWriteConnection($sql, $values); $sequence = $sequence ?: 'id'; $result = (array) $results[0]; - return (int) $result[$sequence]; + $id = $result[$sequence]; + + return is_numeric($id) ? (int) $id : $id; } /** @@ -32,7 +34,7 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu */ public function processColumnListing($results) { - return array_values(array_map(function($r) { return $r->column_name; }, $results)); + return array_values(array_map(function($r) { $r = (object) $r; return $r->column_name; }, $results)); } -} \ No newline at end of file +} diff --git a/Query/Processors/SQLiteProcessor.php b/Query/Processors/SQLiteProcessor.php index dbd3e7e39..34493bf6e 100644 --- a/Query/Processors/SQLiteProcessor.php +++ b/Query/Processors/SQLiteProcessor.php @@ -10,7 +10,7 @@ class SQLiteProcessor extends Processor { */ public function processColumnListing($results) { - return array_values(array_map(function($r) { return $r->name; }, $results)); + return array_values(array_map(function($r) { $r = (object) $r; return $r->name; }, $results)); } } diff --git a/Query/Processors/SqlServerProcessor.php b/Query/Processors/SqlServerProcessor.php index b2d7de7f0..cfdb43263 100755 --- a/Query/Processors/SqlServerProcessor.php +++ b/Query/Processors/SqlServerProcessor.php @@ -33,4 +33,4 @@ public function processColumnListing($results) return array_values(array_map(function($r) { return $r->name; }, $results)); } -} \ No newline at end of file +} diff --git a/QueryException.php b/QueryException.php index 57822d19c..e3f9cf2fe 100644 --- a/QueryException.php +++ b/QueryException.php @@ -1,6 +1,8 @@ sql = $sql; $this->bindings = $bindings; $this->previous = $previous; $this->code = $previous->getCode(); - $this->errorInfo = $previous->errorInfo; $this->message = $this->formatMessage($sql, $bindings, $previous); + + if ($previous instanceof PDOException) + { + $this->errorInfo = $previous->errorInfo; + } } /** @@ -39,7 +47,7 @@ public function __construct($sql, array $bindings, $previous) * * @param string $sql * @param array $bindings - * @param \PDOException $previous + * @param \Exception $previous * @return string */ protected function formatMessage($sql, $bindings, $previous) @@ -67,4 +75,4 @@ public function getBindings() return $this->bindings; } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 5009fc098..ce8901ad1 100755 --- a/README.md +++ b/README.md @@ -27,9 +27,6 @@ use Illuminate\Events\Dispatcher; use Illuminate\Container\Container; $capsule->setEventDispatcher(new Dispatcher(new Container)); -// Set the cache manager instance used by connections... (optional) -$capsule->setCacheManager(...); - // Make this Capsule instance available globally via static methods... (optional) $capsule->setAsGlobal(); diff --git a/SQLiteConnection.php b/SQLiteConnection.php index 5944bdc27..86603fc1e 100755 --- a/SQLiteConnection.php +++ b/SQLiteConnection.php @@ -46,4 +46,4 @@ protected function getDoctrineDriver() return new DoctrineDriver; } -} \ No newline at end of file +} diff --git a/Schema/Blueprint.php b/Schema/Blueprint.php index ba0225ffe..0d91ac6ba 100755 --- a/Schema/Blueprint.php +++ b/Schema/Blueprint.php @@ -38,8 +38,8 @@ class Blueprint { /** * Create a new schema blueprint. * - * @param string $table - * @param Closure $callback + * @param string $table + * @param \Closure|null $callback * @return void */ public function __construct($table, Closure $callback = null) @@ -103,11 +103,16 @@ public function toSql(Connection $connection, Grammar $grammar) */ protected function addImpliedCommands() { - if (count($this->columns) > 0 && ! $this->creating()) + if (count($this->getAddedColumns()) > 0 && ! $this->creating()) { array_unshift($this->commands, $this->createCommand('add')); } + if (count($this->getChangedColumns()) > 0 && ! $this->creating()) + { + array_unshift($this->commands, $this->createCommand('change')); + } + $this->addFluentIndexes(); } @@ -360,6 +365,18 @@ public function bigIncrements($column) return $this->unsignedBigInteger($column, true); } + /** + * Create a new char column on the table. + * + * @param string $column + * @param int $length + * @return \Illuminate\Support\Fluent + */ + public function char($column, $length = 255) + { + return $this->addColumn('char', $column, compact('length')); + } + /** * Create a new string column on the table. * @@ -514,7 +531,6 @@ public function float($column, $total = 8, $places = 2) * @param int|null $total * @param int|null $places * @return \Illuminate\Support\Fluent - * */ public function double($column, $total = null, $places = null) { @@ -557,6 +573,17 @@ public function enum($column, array $allowed) return $this->addColumn('enum', $column, compact('allowed')); } + /** + * Create a new json column on the table. + * + * @param string $column + * @return \Illuminate\Support\Fluent + */ + public function json($column) + { + return $this->addColumn('json', $column); + } + /** * Create a new date column on the table. * @@ -628,11 +655,11 @@ public function timestamps() /** * Add a "deleted at" timestamp for the table. * - * @return void + * @return \Illuminate\Support\Fluent */ public function softDeletes() { - $this->timestamp('deleted_at')->nullable(); + return $this->timestamp('deleted_at')->nullable(); } /** @@ -650,13 +677,26 @@ public function binary($column) * Add the proper columns for a polymorphic table. * * @param string $name + * @param string|null $indexName * @return void */ - public function morphs($name) + public function morphs($name, $indexName = null) { - $this->integer("{$name}_id"); + $this->unsignedInteger("{$name}_id"); $this->string("{$name}_type"); + + $this->index(array("{$name}_id", "{$name}_type"), $indexName); + } + + /** + * Adds the `remember_token` column to the table. + * + * @return \Illuminate\Support\Fluent + */ + public function rememberToken() + { + return $this->string('remember_token', 100)->nullable(); } /** @@ -673,7 +713,7 @@ protected function dropIndexCommand($command, $type, $index) // If the given "index" is actually an array of columns, the developer means // to drop an index merely by specifying the columns involved without the - // conventional name, so we will built the index name from the columns. + // conventional name, so we will build the index name from the columns. if (is_array($index)) { $columns = $index; @@ -742,7 +782,7 @@ protected function addColumn($type, $name, array $parameters = array()) * Remove a column from the schema blueprint. * * @param string $name - * @return \Illuminate\Database\Schema\Blueprint + * @return $this */ public function removeColumn($name) { @@ -791,7 +831,7 @@ public function getTable() } /** - * Get the columns that should be added. + * Get the columns on the blueprint. * * @return array */ @@ -810,4 +850,30 @@ public function getCommands() return $this->commands; } + /** + * Get the columns on the blueprint that should be added. + * + * @return array + */ + public function getAddedColumns() + { + return array_filter($this->columns, function($column) + { + return !$column->change; + }); + } + + /** + * Get the columns on the blueprint that should be changed. + * + * @return array + */ + public function getChangedColumns() + { + return array_filter($this->columns, function($column) + { + return !!$column->change; + }); + } + } diff --git a/Schema/Builder.php b/Schema/Builder.php index 5afc5d352..64a129c3b 100755 --- a/Schema/Builder.php +++ b/Schema/Builder.php @@ -19,6 +19,13 @@ class Builder { */ protected $grammar; + /** + * The Blueprint resolver callback. + * + * @var \Closure + */ + protected $resolver; + /** * Create a new database Schema manager. * @@ -66,7 +73,7 @@ public function hasColumn($table, $column) * @param string $table * @return array */ - protected function getColumnListing($table) + public function getColumnListing($table) { $table = $this->connection->getTablePrefix().$table; @@ -78,8 +85,8 @@ protected function getColumnListing($table) /** * Modify a table on the schema. * - * @param string $table - * @param Closure $callback + * @param string $table + * @param \Closure $callback * @return \Illuminate\Database\Schema\Blueprint */ public function table($table, Closure $callback) @@ -90,8 +97,8 @@ public function table($table, Closure $callback) /** * Create a new table on the schema. * - * @param string $table - * @param Closure $callback + * @param string $table + * @param \Closure $callback * @return \Illuminate\Database\Schema\Blueprint */ public function create($table, Closure $callback) @@ -165,12 +172,17 @@ protected function build(Blueprint $blueprint) /** * Create a new command set with a Closure. * - * @param string $table - * @param Closure $callback + * @param string $table + * @param \Closure|null $callback * @return \Illuminate\Database\Schema\Blueprint */ protected function createBlueprint($table, Closure $callback = null) { + if (isset($this->resolver)) + { + return call_user_func($this->resolver, $table, $callback); + } + return new Blueprint($table, $callback); } @@ -188,7 +200,7 @@ public function getConnection() * Set the database connection instance. * * @param \Illuminate\Database\Connection - * @return \Illuminate\Database\Schema\Builder + * @return $this */ public function setConnection(Connection $connection) { @@ -197,4 +209,15 @@ public function setConnection(Connection $connection) return $this; } -} \ No newline at end of file + /** + * Set the Schema Blueprint resolver callback. + * + * @param \Closure $resolver + * @return void + */ + public function blueprintResolver(Closure $resolver) + { + $this->resolver = $resolver; + } + +} diff --git a/Schema/Grammars/Grammar.php b/Schema/Grammars/Grammar.php index 7d7424674..f0c7688b4 100755 --- a/Schema/Grammars/Grammar.php +++ b/Schema/Grammars/Grammar.php @@ -1,9 +1,12 @@ getDoctrineSchemaManager(); - $column = $connection->getDoctrineColumn($blueprint->getTable(), $command->from); + $table = $this->getTablePrefix().$blueprint->getTable(); + + $column = $connection->getDoctrineColumn($table, $command->from); $tableDiff = $this->getRenamedDiff($blueprint, $command, $column, $schema); @@ -106,14 +111,14 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) /** * Compile the blueprint's column definitions. * - * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Database\Schema\Blueprint $blueprint * @return array */ protected function getColumns(Blueprint $blueprint) { $columns = array(); - foreach ($blueprint->getColumns() as $column) + foreach ($blueprint->getAddedColumns() as $column) { // Each of the column types have their own compiler functions which are tasked // with turning the column definition into its SQL format for this platform @@ -151,6 +156,7 @@ protected function addModifiers($sql, Blueprint $blueprint, Fluent $column) * Get the primary key command if it exists on the blueprint. * * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param string $name * @return \Illuminate\Support\Fluent|null */ protected function getCommandByName(Blueprint $blueprint, $name) @@ -219,16 +225,13 @@ public function wrapTable($table) } /** - * Wrap a value in keyword identifiers. - * - * @param string $value - * @return string + * {@inheritdoc} */ - public function wrap($value) + public function wrap($value, $prefixAlias = false) { if ($value instanceof Fluent) $value = $value->name; - return parent::wrap($value); + return parent::wrap($value, $prefixAlias); } /** @@ -241,7 +244,7 @@ protected function getDefaultValue($value) { if ($value instanceof Expression) return $value; - if (is_bool($value)) return "'".intval($value)."'"; + if (is_bool($value)) return "'".(int) $value."'"; return "'".strval($value)."'"; } @@ -255,11 +258,175 @@ protected function getDefaultValue($value) */ protected function getDoctrineTableDiff(Blueprint $blueprint, SchemaManager $schema) { - $tableDiff = new TableDiff($blueprint->getTable()); + $table = $this->getTablePrefix().$blueprint->getTable(); + + $tableDiff = new TableDiff($table); - $tableDiff->fromTable = $schema->listTableDetails($blueprint->getTable()); + $tableDiff->fromTable = $schema->listTableDetails($table); return $tableDiff; } -} \ No newline at end of file + /** + * Compile a change column command into a series of SQL statements. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @param \Illuminate\Database\Connection $connection + * @return array + */ + public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) + { + $schema = $connection->getDoctrineSchemaManager(); + + $tableDiff = $this->getChangedDiff($blueprint, $schema); + + if ($tableDiff !== false) + { + return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff); + } + + return []; + } + + /** + * Get the Doctrine table difference for the given changes. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return \Doctrine\DBAL\Schema\TableDiff|bool + */ + protected function getChangedDiff(Blueprint $blueprint, SchemaManager $schema) + { + $table = $schema->listTableDetails($this->getTablePrefix().$blueprint->getTable()); + + return (new Comparator)->diffTable($table, $this->getTableWithColumnChanges($blueprint, $table)); + } + + /** + * Get a copy of the given Doctrine table after making the column changes. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Doctrine\DBAL\Schema\Table $table + * @return \Doctrine\DBAL\Schema\TableDiff + */ + protected function getTableWithColumnChanges(Blueprint $blueprint, Table $table) + { + $table = clone $table; + + foreach($blueprint->getChangedColumns() as $fluent) + { + $column = $this->getDoctrineColumnForChange($table, $fluent); + + // Here we will spin through each fluent column definition and map it to the proper + // Doctrine column definitions, which is necessasry because Laravel and Doctrine + // use some different terminology for various column attributes on the tables. + foreach ($fluent->getAttributes() as $key => $value) + { + if ( ! is_null($option = $this->mapFluentOptionToDoctrine($key))) + { + if (method_exists($column, $method = 'set'.ucfirst($option))) + { + $column->{$method}($this->mapFluentValueToDoctrine($option, $value)); + } + } + } + } + + return $table; + } + + /** + * Get the Doctrine column instance for a column change. + * + * @param \Doctrine\DBAL\Schema\Table $table + * @param \Illuminate\Support\Fluent $fluent + * @return \Doctrine\DBAL\Schema\Column + */ + protected function getDoctrineColumnForChange(Table $table, Fluent $fluent) + { + return $table->changeColumn( + $fluent['name'], $this->getDoctrineColumnChangeOptions($fluent) + )->getColumn($fluent['name']); + } + + /** + * Get the Doctrine column change options. + * + * @param \Illuminate\Support\Fluent $fluent + * @return array + */ + protected function getDoctrineColumnChangeOptions(Fluent $fluent) + { + $options = ['type' => Type::getType($this->getType($fluent))]; + + if (in_array($fluent['type'], ['text', 'mediumText', 'longText'])) + { + $options['length'] = $this->calculateDoctrineTextLength($fluent['type']); + } + + return $options; + } + + /** + * Calculate the proper column length to force the Doctrine text type. + * + * @param string $type + * @return int + */ + protected function calculateDoctrineTextLength($type) + { + switch ($type) + { + case 'mediumText': + return 65535 + 1; + + case 'longText': + return 16777215 + 1; + + default: + return 255 + 1; + } + } + + /** + * Get the matching Doctrine option for a given Fluent attribute name. + * + * @param string $attribute + * @return string + */ + protected function mapFluentOptionToDoctrine($attribute) + { + switch($attribute) + { + case 'type': + case 'name': + return; + + case 'nullable': + return 'notnull'; + + case 'total': + return 'precision'; + + case 'places': + return 'scale'; + + default: + return $attribute; + } + } + + /** + * Get the matching Doctrine value for a given Fluent attribute. + * + * @param string $option + * @param mixed $value + * @return mixed + */ + protected function mapFluentValueToDoctrine($option, $value) + { + return $option == 'notnull' ? ! $value : $value; + } + +} diff --git a/Schema/Grammars/MySqlGrammar.php b/Schema/Grammars/MySqlGrammar.php index 7de0007d9..64cbb55ca 100755 --- a/Schema/Grammars/MySqlGrammar.php +++ b/Schema/Grammars/MySqlGrammar.php @@ -6,26 +6,19 @@ class MySqlGrammar extends Grammar { - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '`%s`'; - /** * The possible column modifiers. * * @var array */ - protected $modifiers = array('Unsigned', 'Nullable', 'Default', 'Increment', 'After'); + protected $modifiers = array('Unsigned', 'Nullable', 'Default', 'Increment', 'Comment', 'After'); /** * The possible column serials * * @var array */ - protected $serials = array('bigInteger', 'integer'); + protected $serials = array('bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger'); /** * Compile the query to determine the list of tables. @@ -40,7 +33,6 @@ public function compileTableExists() /** * Compile the query to determine the list of columns. * - * @param string $table * @return string */ public function compileColumnExists() @@ -98,7 +90,7 @@ protected function compileCreateEncoding($sql, Connection $connection) } /** - * Compile a create table command. + * Compile an add column command. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command @@ -276,6 +268,17 @@ public function compileRename(Blueprint $blueprint, Fluent $command) return "rename table {$from} to ".$this->wrapTable($command->to); } + /** + * Create the column definition for a char type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeChar(Fluent $column) + { + return "char({$column->length})"; + } + /** * Create the column definition for a string type. * @@ -383,7 +386,7 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - return "float({$column->total}, {$column->places})"; + return $this->typeDouble($column); } /** @@ -398,10 +401,8 @@ protected function typeDouble(Fluent $column) { return "double({$column->total}, {$column->places})"; } - else - { - return 'double'; - } + + return 'double'; } /** @@ -427,7 +428,7 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for a enum type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -437,6 +438,17 @@ protected function typeEnum(Fluent $column) return "enum('".implode("', '", $column->allowed)."')"; } + /** + * Create the column definition for a json type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeJson(Fluent $column) + { + return 'text'; + } + /** * Create the column definition for a date type. * @@ -563,4 +575,32 @@ protected function modifyAfter(Blueprint $blueprint, Fluent $column) } } + /** + * Get the SQL for an "comment" column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyComment(Blueprint $blueprint, Fluent $column) + { + if ( ! is_null($column->comment)) + { + return ' comment "'.$column->comment.'"'; + } + } + + /** + * Wrap a single string in keyword identifiers. + * + * @param string $value + * @return string + */ + protected function wrapValue($value) + { + if ($value === '*') return $value; + + return '`'.str_replace('`', '``', $value).'`'; + } + } diff --git a/Schema/Grammars/PostgresGrammar.php b/Schema/Grammars/PostgresGrammar.php index c17057944..de63407ec 100755 --- a/Schema/Grammars/PostgresGrammar.php +++ b/Schema/Grammars/PostgresGrammar.php @@ -5,13 +5,6 @@ class PostgresGrammar extends Grammar { - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '"%s"'; - /** * The possible column modifiers. * @@ -229,6 +222,17 @@ public function compileRename(Blueprint $blueprint, Fluent $command) return "alter table {$from} rename to ".$this->wrapTable($command->to); } + /** + * Create the column definition for a char type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeChar(Fluent $column) + { + return "char({$column->length})"; + } + /** * Create the column definition for a string type. * @@ -336,7 +340,7 @@ protected function typeSmallInteger(Fluent $column) */ protected function typeFloat(Fluent $column) { - return 'real'; + return $this->typeDouble($column); } /** @@ -382,7 +386,18 @@ protected function typeEnum(Fluent $column) { $allowed = array_map(function($a) { return "'".$a."'"; }, $column->allowed); - return "varchar(255) check ({$column->name} in (".implode(', ', $allowed)."))"; + return "varchar(255) check (\"{$column->name}\" in (".implode(', ', $allowed)."))"; + } + + /** + * Create the column definition for a json type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeJson(Fluent $column) + { + return "json"; } /** @@ -404,7 +419,7 @@ protected function typeDate(Fluent $column) */ protected function typeDateTime(Fluent $column) { - return 'timestamp'; + return 'timestamp(0) without time zone'; } /** @@ -415,7 +430,7 @@ protected function typeDateTime(Fluent $column) */ protected function typeTime(Fluent $column) { - return 'time'; + return 'time(0) without time zone'; } /** @@ -426,7 +441,7 @@ protected function typeTime(Fluent $column) */ protected function typeTimestamp(Fluent $column) { - return 'timestamp'; + return 'timestamp(0) without time zone'; } /** @@ -476,7 +491,7 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) */ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { - if (in_array($column->type, $this->serials) and $column->autoIncrement) + if (in_array($column->type, $this->serials) && $column->autoIncrement) { return ' primary key'; } diff --git a/Schema/Grammars/SQLiteGrammar.php b/Schema/Grammars/SQLiteGrammar.php index 32a26491c..b0c279eab 100755 --- a/Schema/Grammars/SQLiteGrammar.php +++ b/Schema/Grammars/SQLiteGrammar.php @@ -6,13 +6,6 @@ class SQLiteGrammar extends Grammar { - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '"%s"'; - /** * The possible column modifiers. * @@ -68,7 +61,7 @@ public function compileCreate(Blueprint $blueprint, Fluent $command) $sql .= (string) $this->addPrimaryKeys($blueprint); - return $sql .= ')'; + return $sql.')'; } /** @@ -155,6 +148,8 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) $columns = $this->prefixArray('add column', $this->getColumns($blueprint)); + $statements = array(); + foreach ($columns as $column) { $statements[] = 'alter table '.$table.' '.$column; @@ -293,6 +288,17 @@ public function compileRename(Blueprint $blueprint, Fluent $command) return "alter table {$from} rename to ".$this->wrapTable($command->to); } + /** + * Create the column definition for a char type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeChar(Fluent $column) + { + return 'varchar'; + } + /** * Create the column definition for a string type. * @@ -422,7 +428,7 @@ protected function typeDouble(Fluent $column) */ protected function typeDecimal(Fluent $column) { - return 'float'; + return 'numeric'; } /** @@ -437,7 +443,7 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for a enum type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -447,6 +453,17 @@ protected function typeEnum(Fluent $column) return 'varchar'; } + /** + * Create the column definition for a json type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeJson(Fluent $column) + { + return 'text'; + } + /** * Create the column definition for a date type. * @@ -538,7 +555,7 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) */ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { - if (in_array($column->type, $this->serials) and $column->autoIncrement) + if (in_array($column->type, $this->serials) && $column->autoIncrement) { return ' primary key autoincrement'; } diff --git a/Schema/Grammars/SqlServerGrammar.php b/Schema/Grammars/SqlServerGrammar.php index 68f4179e2..89dd14e50 100755 --- a/Schema/Grammars/SqlServerGrammar.php +++ b/Schema/Grammars/SqlServerGrammar.php @@ -5,13 +5,6 @@ class SqlServerGrammar extends Grammar { - /** - * The keyword identifier wrapper format. - * - * @var string - */ - protected $wrapper = '"%s"'; - /** * The possible column modifiers. * @@ -44,8 +37,8 @@ public function compileTableExists() */ public function compileColumnExists($table) { - return "select col.name from sys.columns as col - join sys.objects as obj on col.object_id = obj.object_id + return "select col.name from sys.columns as col + join sys.objects as obj on col.object_id = obj.object_id where obj.type = 'U' and obj.name = '$table'"; } @@ -164,8 +157,6 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - $table = $blueprint->getTable(); - $table = $this->wrapTable($blueprint); return "alter table {$table} drop constraint {$command->index}"; @@ -227,6 +218,17 @@ public function compileRename(Blueprint $blueprint, Fluent $command) return "sp_rename {$from}, ".$this->wrapTable($command->to); } + /** + * Create the column definition for a char type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeChar(Fluent $column) + { + return "nchar({$column->length})"; + } + /** * Create the column definition for a string type. * @@ -371,7 +373,7 @@ protected function typeBoolean(Fluent $column) } /** - * Create the column definition for a enum type. + * Create the column definition for an enum type. * * @param \Illuminate\Support\Fluent $column * @return string @@ -381,6 +383,17 @@ protected function typeEnum(Fluent $column) return 'nvarchar(255)'; } + /** + * Create the column definition for a json type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeJson(Fluent $column) + { + return 'nvarchar(max)'; + } + /** * Create the column definition for a date type. * @@ -472,7 +485,7 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) */ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { - if (in_array($column->type, $this->serials) and $column->autoIncrement) + if (in_array($column->type, $this->serials) && $column->autoIncrement) { return ' identity primary key'; } diff --git a/Schema/MySqlBuilder.php b/Schema/MySqlBuilder.php index a0a1d2e0f..747268154 100755 --- a/Schema/MySqlBuilder.php +++ b/Schema/MySqlBuilder.php @@ -25,7 +25,7 @@ public function hasTable($table) * @param string $table * @return array */ - protected function getColumnListing($table) + public function getColumnListing($table) { $sql = $this->grammar->compileColumnExists(); diff --git a/SeedServiceProvider.php b/SeedServiceProvider.php index 859c9f1ef..3dd93c177 100755 --- a/SeedServiceProvider.php +++ b/SeedServiceProvider.php @@ -21,7 +21,7 @@ public function register() { $this->registerSeedCommand(); - $this->app->bindShared('seeder', function($app) + $this->app->singleton('seeder', function() { return new Seeder; }); @@ -36,7 +36,7 @@ public function register() */ protected function registerSeedCommand() { - $this->app->bindShared('command.seed', function($app) + $this->app->singleton('command.seed', function($app) { return new SeedCommand($app['db']); }); @@ -52,4 +52,4 @@ public function provides() return array('seeder', 'command.seed'); } -} \ No newline at end of file +} diff --git a/Seeder.php b/Seeder.php index 5a378856d..9074dce98 100755 --- a/Seeder.php +++ b/Seeder.php @@ -36,7 +36,10 @@ public function call($class) { $this->resolve($class)->run(); - $this->command->getOutput()->writeln("Seeded: $class"); + if (isset($this->command)) + { + $this->command->getOutput()->writeln("Seeded: $class"); + } } /** @@ -51,19 +54,26 @@ protected function resolve($class) { $instance = $this->container->make($class); - return $instance->setContainer($this->container)->setCommand($this->command); + $instance->setContainer($this->container); } else { - return new $class; + $instance = new $class; + } + + if (isset($this->command)) + { + $instance->setCommand($this->command); } + + return $instance; } /** * Set the IoC container instance. * * @param \Illuminate\Container\Container $container - * @return void + * @return $this */ public function setContainer(Container $container) { @@ -76,7 +86,7 @@ public function setContainer(Container $container) * Set the console command instance. * * @param \Illuminate\Console\Command $command - * @return void + * @return $this */ public function setCommand(Command $command) { @@ -85,4 +95,4 @@ public function setCommand(Command $command) return $this; } -} \ No newline at end of file +} diff --git a/SqlServerConnection.php b/SqlServerConnection.php index 47d3ac252..523f7598f 100755 --- a/SqlServerConnection.php +++ b/SqlServerConnection.php @@ -1,6 +1,7 @@ pdo->exec('ROLLBACK TRAN'); diff --git a/composer.json b/composer.json index 4f05bc08c..a8ff505c2 100755 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "illuminate/database", + "description": "The Illuminate Database package.", "license": "MIT", "keywords": ["laravel", "database", "sql", "orm"], "authors": [ @@ -9,31 +10,26 @@ } ], "require": { - "php": ">=5.3.0", - "illuminate/container": "4.1.x", - "illuminate/events": "4.1.x", - "illuminate/support": "4.1.x", - "nesbot/carbon": "1.*" - }, - "require-dev": { - "illuminate/cache": "4.1.x", - "illuminate/console": "4.1.x", - "illuminate/filesystem": "4.1.x", - "illuminate/pagination": "4.1.x", - "illuminate/support": "4.1.x", - "mockery/mockery": "0.7.2", - "phpunit/phpunit": "3.7.*" + "php": ">=5.4.0", + "illuminate/container": "5.1.*", + "illuminate/contracts": "5.1.*", + "illuminate/support": "5.1.*", + "nesbot/carbon": "~1.0" }, "autoload": { - "psr-0": { - "Illuminate\\Database": "" + "psr-4": { + "Illuminate\\Database\\": "" } }, - "target-dir": "Illuminate/Database", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "5.1-dev" } }, + "suggest": { + "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.4).", + "illuminate/console": "Required to use the database commands (5.1.*).", + "illuminate/filesystem": "Required to use the migrations (5.1.*)." + }, "minimum-stability": "dev" }