From d6d667af902e209b893c6bb72227a9a6db519602 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 16 Mar 2012 08:37:31 -0500 Subject: [PATCH] Added Eloquent 2. Signed-off-by: Taylor Otwell --- application/config/application.php | 1 + laravel/database/eloquent/model.php | 528 ++++++++++++++++++ laravel/database/eloquent/query.php | 246 ++++++++ .../eloquent/relationships/belongs_to.php | 83 +++ .../eloquent/relationships/has_many.php | 47 ++ .../relationships/has_many_and_belongs_to.php | 239 ++++++++ .../eloquent/relationships/has_one.php | 47 ++ .../relationships/has_one_or_many.php | 39 ++ .../eloquent/relationships/relationship.php | 78 +++ 9 files changed, 1308 insertions(+) create mode 100644 laravel/database/eloquent/model.php create mode 100644 laravel/database/eloquent/query.php create mode 100644 laravel/database/eloquent/relationships/belongs_to.php create mode 100644 laravel/database/eloquent/relationships/has_many.php create mode 100644 laravel/database/eloquent/relationships/has_many_and_belongs_to.php create mode 100644 laravel/database/eloquent/relationships/has_one.php create mode 100644 laravel/database/eloquent/relationships/has_one_or_many.php create mode 100644 laravel/database/eloquent/relationships/relationship.php diff --git a/application/config/application.php b/application/config/application.php index 43a52e86..4c1a769f 100644 --- a/application/config/application.php +++ b/application/config/application.php @@ -126,6 +126,7 @@ 'Cookie' => 'Laravel\\Cookie', 'Crypter' => 'Laravel\\Crypter', 'DB' => 'Laravel\\Database', + 'Eloquent' => 'Laravel\\Database\\Eloquent\\Model', 'Event' => 'Laravel\\Event', 'File' => 'Laravel\\File', 'Filter' => 'Laravel\\Routing\\Filter', diff --git a/laravel/database/eloquent/model.php b/laravel/database/eloquent/model.php new file mode 100644 index 00000000..32d3933c --- /dev/null +++ b/laravel/database/eloquent/model.php @@ -0,0 +1,528 @@ +exists = $exists; + + $this->fill($attributes); + } + + /** + * Set the accessible attributes for the given model. + * + * @param array $attributes + * @return void + */ + public static function accessible($attributes) + { + static::$accessible = $attributes; + } + + /** + * Hydrate the model with an array of attributes. + * + * @param array $attributes + * @return Model + */ + public function fill($attributes) + { + $attributes = (array) $attributes; + + foreach ($attributes as $key => $value) + { + // If the "accessible" property is an array, the developer is limiting the + // attributes that may be mass assigned, and we need to verify that the + // current attribute is included in that list of allowed attributes. + if (is_array(static::$accessible)) + { + if (in_array($key, static::$accessible)) + { + $this->$key = $value; + } + } + + // If the "accessible" property is not an array, no attributes have been + // white-listed and we are free to set the value of the attribute to + // the value that has been passed into the method without a check. + else + { + $this->$key = $value; + } + } + + // If the original attribute values have not been set, we will set them to + // the values passed to this method allowing us to quickly check if the + // model has changed since hydration of the original instance. + if (count($this->original) === 0) + { + $this->original = $this->attributes; + } + + return $this; + } + + /** + * Find a model by its primary key. + * + * @param string $id + * @param array $columns + * @return Model + */ + public static function find($id, $columns = array('*')) + { + $model = new static; + + return $model->query()->where(static::$key, '=', $id)->first($columns); + } + + /** + * Get all of the models in the database. + * + * @return array + */ + public static function all() + { + $model = new static; + + return $model->query()->get(); + } + + /** + * The relationships that should be eagerly loaded by the query. + * + * @param array $includes + * @return Model + */ + public function _with($includes) + { + $this->includes = (array) $includes; + + return $this; + } + + /** + * Get the query for a one-to-one association. + * + * @param string $model + * @param string $foreign + * @return Relationship + */ + public function has_one($model, $foreign = null) + { + return $this->has_one_or_many(__FUNCTION__, $model, $foreign); + } + + /** + * Get the query for a one-to-many association. + * + * @param string $model + * @param string $foreign + * @return Relationship + */ + public function has_many($model, $foreign = null) + { + return $this->has_one_or_many(__FUNCTION__, $model, $foreign); + } + + /** + * Get the query for a one-to-one / many association. + * + * @param string $type + * @param string $model + * @param string $foreign + * @return Relationship + */ + protected function has_one_or_many($type, $model, $foreign) + { + if ($type == 'has_one') + { + return new Relationships\Has_One($this, $model, $foreign); + } + else + { + return new Relationships\Has_Many($this, $model, $foreign); + } + } + + /** + * Get the query for a one-to-one (inverse) relationship. + * + * @param string $model + * @param string $foreign + * @return Relationship + */ + public function belongs_to($model, $foreign = null) + { + // If no foreign key is specified for the relationship, we will assume that the + // name of the calling function matches the foreign key. For example, if the + // calling function is "manager", we'll assume the key is "manager_id". + if (is_null($foreign)) + { + list(, $caller) = debug_backtrace(false); + + $foreign = "{$caller['function']}_id"; + } + + return new Relationships\Belongs_To($this, $model, $foreign); + } + + /** + * Get the query for a many-to-many relationship. + * + * @param string $model + * @param string $table + * @param string $foreign + * @param string $other + * @return Relationship + */ + public function has_many_and_belongs_to($model, $table, $foreign = null, $other = null) + { + return new Has_Many_And_Belongs_To($this, $model, $table, $foreign, $other); + } + + /** + * Save the model instance to the database. + * + * @return bool + */ + public function save() + { + if ( ! $this->dirty()) return true; + + if (static::$timestamps) + { + $this->timestamp(); + } + + // If the model exists, we only need to update it in the database, and the update + // will be considered successful if there is one affected row returned from the + // fluent query instance. We'll set the where condition automatically. + if ($this->exists) + { + $query = $this->query()->where(static::$key, '=', $this->get_key()); + + $result = $query->update($this->get_dirty()) === 1; + } + + // If the model does not exist, we will insert the record and retrieve the last + // insert ID that is associated with the model. If the ID returned is numeric + // then we can consider the insert successful. + else + { + $id = $this->query()->insert_get_id($this->attributes, $this->sequence()); + + $this->set_key($id); + + $this->exists = $result = is_numeric($this->get_key()); + } + + // After the model has been "saved", we will set the original attributes to + // match the current attributes so the model will not be viewed as being + // dirty and subsequent calls won't hit the database. + $this->original = $this->attributes; + + return $result; + } + + /** + * Set the update and creation timestamps on the model. + * + * @return void + */ + protected function timestamp() + { + $this->updated_at = $this->get_timestamp(); + + if ( ! $this->exists) $this->created_at = $this->updated_at; + } + + /** + * Get the current timestamp in its storable form. + * + * @return mixed + */ + protected function get_timestamp() + { + return date('Y-m-d H:i:s'); + } + + /** + * Get a new fluent query builder instance for the model. + * + * @return Query + */ + protected function query() + { + return new Query($this); + } + + /** + * Determine if a given attribute has changed from its original state. + * + * @param string $attribute + * @return bool + */ + public function changed($attribute) + { + array_get($this->attributes, $attribute) !== array_get($this->original, $attribute); + } + + /** + * Determine if the model has been changed from its original state. + * + * Models that haven't been persisted to storage are always considered dirty. + * + * @return bool + */ + public function dirty() + { + return ! $this->exists or $this->original !== $this->attributes; + } + + /** + * Get the dirty attributes for the model. + * + * @return array + */ + public function get_dirty() + { + return array_diff_assoc($this->attributes, $this->original); + } + + /** + * Get the value of the primary key for the model. + * + * @return int + */ + public function get_key() + { + return $this->get_attribute(static::$key); + } + + /** + * Set the value of the primary key for the model. + * + * @param int $value + * @return void + */ + public function set_key($value) + { + return $this->set_attribute(static::$key, $value); + } + + /** + * Get a given attribute from the model. + * + * @param string $key + */ + public function get_attribute($key) + { + return $this->attributes[$key]; + } + + /** + * Set an attribute's value on the model. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function set_attribute($key, $value) + { + $this->attributes[$key] = $value; + } + + /** + * Handle the dynamic retrieval of attributes and associations. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + // First we will check to see if the requested key is an already loaded + // relationship and return it if it is. All relationships are stored + // in the special relationships array so they are not persisted. + if (array_key_exists($key, $this->relationships)) + { + return $this->relationships[$key]; + } + + // If the item is not a loaded relationship, it may be a relationship + // that hasn't been loaded yet. If it is, we will lazy load it and + // set the value of the relationship in the relationship array. + elseif (method_exists($this, $key)) + { + return $this->relationships[$key] = $this->$key()->results(); + } + + // Finally we will just assume the requested key is just a regular + // attribute and attempt to call the getter method for it, which + // will fall into the __call method if one doesn't exist. + else + { + return $this->{"get_{$key}"}(); + } + } + + /** + * Handle the dynamic setting of attributes. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function __set($key, $value) + { + $this->{"set_{$key}"}($value); + } + + /** + * Handle dynamic method calls on the model. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + // If the method is actually the name of a static property on the model we'll + // return the value of the static property. This makes it convenient for + // relationships to access these values off of the instances. + if (in_array($method, array('key', 'table', 'connection', 'sequence'))) + { + return static::$$method; + } + + // Some methods need to be accessed both staticly and non-staticly so we'll + // keep underscored methods of those methods and intercept calls to them + // here so they can be called either way on the model instance. + if (in_array($method, array('with'))) + { + return call_user_func_array(array($this, '_'.$method), $parameters); + } + + // First we want to see if the method is a getter / setter for an attribute. + // If it is, we'll call the basic getter and setter method for the model + // to perform the appropriate action based on the method. + if (starts_with($method, 'get_')) + { + return $this->get_attribute(substr($method, 4)); + } + elseif (starts_with($method, 'set_')) + { + return $this->set_attribute(substr($method, 4), $parameters[0]); + } + // Finally we will assume that the method is actually the beginning of a + // query, such as "where", and will create a new query instance and + // call the method on the query instance, returning it after. + else + { + return call_user_func_array(array($this->query(), $method), $parameters); + } + } + + /** + * Dynamically handle static method calls on the model. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public static function __callStatic($method, $parameters) + { + $model = get_called_class(); + + return call_user_func_array(array(new $model, $method), $parameters); + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/query.php b/laravel/database/eloquent/query.php new file mode 100644 index 00000000..f590ad9e --- /dev/null +++ b/laravel/database/eloquent/query.php @@ -0,0 +1,246 @@ +model = ($model instanceof Model) ? $model : new $model; + + $this->table = $this->query(); + } + + /** + * Get the first model result for the query. + * + * @param array $columns + * @return mixed + */ + public function first($columns = array('*')) + { + $results = $this->hydrate($this->model, $this->table->take(1)->get($columns, false)); + + return (is_array($results)) ? head($results) : $results; + } + + /** + * Get all of the model results for the query. + * + * @param array $columns + * @param bool $include + * @return array + */ + public function get($columns = array('*'), $include = true) + { + $results = $this->hydrate($this->model, $this->table->get($columns)); + + if ($include) + { + foreach ($this->model_includes() as $relationship => $constraints) + { + // If the relationship is nested, we will skip laoding it here and let + // the load method parse and set the nested eager loads on the right + // relationship when it is getting ready to eager laod it. + if (str_contains($relationship, '.')) + { + continue; + } + + $this->load($results, $relationship, $constraints); + } + } + + return $results; + } + + /** + * Hydrate an array of models from the given results. + * + * @param Model $model + * @param array $results + * @return array + */ + public function hydrate($model, $results) + { + $class = get_class($model); + + $models = array(); + + // We'll spin through the array of database results and hydrate a model + // for each one of the records. We will also set the "exists" flag to + // "true" so that the model will be updated when it is saved. + foreach ((array) $results as $result) + { + $result = (array) $result; + + $models[$result[$this->model->key()]] = new $class($result, true); + } + + return $models; + } + + /** + * Hydrate an eagerly loaded relationship on the model results. + * + * @param array $results + * @param string $relationship + * @param array|null $constraints + * @return void + */ + protected function load(&$results, $relationship, $constraints) + { + $query = $this->model->$relationship(); + + $query->model->includes = $this->nested_includes($relationship); + + // We'll remove any of the where clauses from the relationship to give + // the relationship the opportunity to set the constraints for an + // eager relationship using a separate, specific method. + $query->table->reset_where(); + + $query->eagerly_constrain($results); + + // Constraints may be specified in-line for the eager load by passing + // a Closure as the value portion of the eager load. We can use the + // query builder's nested query support to add the constraints. + if ( ! is_null($constraints)) + { + $query->table->where_nested($constraints); + } + + // Before matching the models, we will initialize the relationship + // to either null for single-value relationships or an array for + // the multi-value relationships as their baseline value. + $query->initialize($results, $relationship); + + $query->match($relationship, $results, $query->get()); + } + + /** + * Gather the nested includes for a given relationship. + * + * @param string $relationship + * @return array + */ + protected function nested_includes($relationship) + { + $nested = array(); + + foreach ($this->model_includes() as $include => $constraints) + { + // To get the nested includes, we want to find any includes that begin + // the relationship and a dot, then we will strip off the leading + // nesting indicator and set the include in the array. + if (starts_with($include, $relationship.'.')) + { + $nested[substr($include, strlen($relationship.'.'))] = $constraints; + } + } + + return $nested; + } + + /** + * Get the eagerly loaded relationships for the model. + * + * @return array + */ + protected function model_includes() + { + $includes = array(); + + foreach ($this->model->includes as $relationship => $constraints) + { + // When eager loading relationships, constraints may be set on the eager + // load definition; however, is none are set, we need to swap the key + // and the value of the array since there are no constraints. + if (is_numeric($relationship)) + { + list($relationship, $constraints) = array($constraints, null); + } + + $includes[$relationship] = $constraints; + } + + return $includes; + } + + /** + * Get a fluent query builder for the model. + * + * @return Query + */ + protected function query() + { + return $this->connection()->table($this->model->table()); + } + + /** + * Get the database connection for the model. + * + * @return Connection + */ + protected function connection() + { + return Database::connection($this->model->connection()); + } + + /** + * Handle dynamic method calls to the query. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + $result = call_user_func_array(array($this->table, $method), $parameters); + + // Some methods may get their results straight from the fluent query + // builder, such as the aggregate methods. If the called method is + // one of these, we will return the result straight away. + if (in_array($method, $this->passthru)) + { + return $result; + } + + return $this; + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/relationships/belongs_to.php b/laravel/database/eloquent/relationships/belongs_to.php new file mode 100644 index 00000000..97c001e8 --- /dev/null +++ b/laravel/database/eloquent/relationships/belongs_to.php @@ -0,0 +1,83 @@ +base->get_attribute($this->foreign); + + $this->table->where($this->base->key(), '=', $foreign); + } + + /** + * Initialize a relationship on an array of parent models. + * + * @param array $parents + * @param string $relationship + * @return void + */ + public function initialize(&$parents, $relationship) + { + foreach ($parents as &$parent) + { + $parent->relationships[$relationship] = null; + } + } + + /** + * Set the proper constraints on the relationship table for an eager load. + * + * @param array $results + * @return void + */ + public function eagerly_constrain($results) + { + $keys = array(); + + // Inverse one-to-many relationships require us to gather the keys from the + // parent models and use those keys when setting the constraint since we + // are looking for the parent of a child model in this relationship. + foreach ($results as $result) + { + $keys[] = $result->{$this->foreign_key()}; + } + + $this->table->where_in($this->model->key(), array_unique($keys)); + } + + /** + * Match eagerly loaded child models to their parent models. + * + * @param array $children + * @param array $parents + * @return void + */ + public function match($relationship, &$children, $parents) + { + $foreign = $this->foreign_key(); + + foreach ($children as &$child) + { + if (array_key_exists($child->$foreign, $parents)) + { + $child->relationships[$relationship] = $parents[$child->$foreign]; + } + } + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/relationships/has_many.php b/laravel/database/eloquent/relationships/has_many.php new file mode 100644 index 00000000..b80d3844 --- /dev/null +++ b/laravel/database/eloquent/relationships/has_many.php @@ -0,0 +1,47 @@ +relationships[$relationship] = array(); + } + } + + /** + * Match eagerly loaded child models to their parent models. + * + * @param array $parents + * @param array $children + * @return void + */ + public function match($relationship, &$parents, $children) + { + $foreign = $this->foreign_key(); + + foreach ($children as $key => $child) + { + $parents[$child->$foreign]->relationships[$relationship][$child->get_key()] = $child; + } + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/relationships/has_many_and_belongs_to.php b/laravel/database/eloquent/relationships/has_many_and_belongs_to.php new file mode 100644 index 00000000..32ac5e69 --- /dev/null +++ b/laravel/database/eloquent/relationships/has_many_and_belongs_to.php @@ -0,0 +1,239 @@ +other = $other; + + $this->joining = $table; + + parent::__construct($model, $associated, $foreign); + } + + /** + * Get the properly hydrated results for the relationship. + * + * @return array + */ + public function results() + { + return parent::get(); + } + + /** + * Insert a new record into the joining table of the association. + * + * @param int $id + * @return bool + */ + public function add($id) + { + return $this->insert_joining($this->join_record($id)); + } + + /** + * Insert a new record for the association. + * + * @param array $attributes + * @return bool + */ + public function insert($attributes) + { + $id = $this->table->insert_get_id($attributes, $this->model->sequence()); + + $result = $this->insert_joining($this->join_record($id)); + + return is_numeric($id) and $result; + } + + /** + * Delete all of the records from the joining table for the model. + * + * @return int + */ + public function delete() + { + return $this->joining_table()->where($this->foreign_key(), '=', $this->base->get_key())->delete(); + } + + /** + * Create an array representing a new joining record for the association. + * + * @param int $id + * @return array + */ + protected function join_record($id) + { + return array($this->foreign_key() => $this->base->get_key(), $this->other_key() => $id); + } + + /** + * Insert a new record into the joining table of the association. + * + * @param array $attributes + * @return void + */ + protected function insert_joining($attributes) + { + return $this->joining_table()->insert($attributes); + } + + /** + * Get a fluent query for the joining table of the relationship. + * + * @return Query + */ + protected function joining_table() + { + return $this->connection()->table($this->joining); + } + + /** + * Set the proper constraints on the relationship table. + * + * @return void + */ + protected function constrain() + { + $foreign = $this->foreign_key(); + + $this->set_select($foreign)->set_join($this->other_key())->set_where($foreign); + } + + /** + * Set the SELECT clause on the query builder for the relationship. + * + * @param string $foreign + * @return void + */ + protected function set_select($foreign) + { + $foreign = $this->joining.'.'.$foreign.' as eloquent_foreign_key'; + + $this->table->select(array($this->model->table().'.*', $foreign)); + + return $this; + } + + /** + * Set the JOIN clause on the query builder for the relationship. + * + * @param string $other + * @return void + */ + protected function set_join($other) + { + $this->table->join($this->joining, $this->associated_key(), '=', $this->joining.'.'.$other); + + return $this; + } + + /** + * Set the WHERE clause on the query builder for the relationship. + * + * @param string $foreign + * @return void + */ + protected function set_where($foreign) + { + $this->table->where($this->joining.'.'.$foreign, '=', $this->base->get_key()); + + return $this; + } + + /** + * Initialize a relationship on an array of parent models. + * + * @param array $parents + * @param string $relationship + * @return void + */ + public function initialize(&$parents, $relationship) + { + foreach ($parents as &$parent) + { + $parent->relationships[$relationship] = array(); + } + } + + /** + * Set the proper constraints on the relationship table for an eager load. + * + * @param array $results + * @return void + */ + public function eagerly_constrain($results) + { + $this->table->where_in($this->joining.'.'.$this->foreign_key(), array_keys($results)); + } + + /** + * Match eagerly loaded child models to their parent models. + * + * @param array $parents + * @param array $children + * @return void + */ + public function match($relationship, &$parents, $children) + { + $foreign = 'eloquent_foreign_key'; + + foreach ($children as $key => $child) + { + $parents[$child->$foreign]->relationships[$relationship][$child->{$child->key()}] = $child; + + // After matching the child model with its parent, we can remove the foreign key + // from the model, as it was only necessary to allow us to know which parent + // the child belongs to for eager loading and isn't necessary otherwise. + unset($child->attributes[$foreign]); + + unset($child->original[$foreign]); + } + } + + /** + * Get the other or associated key for the relationship. + * + * @return string + */ + protected function other_key() + { + return Relationship::foreign($this->model, $this->other); + } + + /** + * Get the fully qualified associated table's primary key. + * + * @return string + */ + protected function associated_key() + { + return $this->model->table().'.'.$this->model->key(); + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/relationships/has_one.php b/laravel/database/eloquent/relationships/has_one.php new file mode 100644 index 00000000..077c3ada --- /dev/null +++ b/laravel/database/eloquent/relationships/has_one.php @@ -0,0 +1,47 @@ +relationships[$relationship] = null; + } + } + + /** + * Match eagerly loaded child models to their parent models. + * + * @param array $parents + * @param array $children + * @return void + */ + public function match($relationship, &$parents, $children) + { + $foreign = $this->foreign_key(); + + foreach ($children as $key => $child) + { + $parents[$child->$foreign]->relationships[$relationship] = $child; + } + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/relationships/has_one_or_many.php b/laravel/database/eloquent/relationships/has_one_or_many.php new file mode 100644 index 00000000..a64d2bdf --- /dev/null +++ b/laravel/database/eloquent/relationships/has_one_or_many.php @@ -0,0 +1,39 @@ +foreign_key()] = $this->base->get_key(); + + return parent::insert($attributes); + } + + /** + * Set the proper constraints on the relationship table. + * + * @return void + */ + protected function constrain() + { + $this->table->where($this->foreign_key(), '=', $this->base->get_key()); + } + + /** + * Set the proper constraints on the relationship table for an eager load. + * + * @param array $results + * @return void + */ + public function eagerly_constrain($results) + { + $this->table->where_in($this->foreign_key(), array_keys($results)); + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/relationships/relationship.php b/laravel/database/eloquent/relationships/relationship.php new file mode 100644 index 00000000..80356762 --- /dev/null +++ b/laravel/database/eloquent/relationships/relationship.php @@ -0,0 +1,78 @@ +foreign = $foreign; + + // We will go ahead and set the model and associated instances on the relationship + // to match the relationship targets passed in from the model. These will allow + // us to gather more inforamtion on the relationship. + $this->model = ($associated instanceof Model) ? $associated : new $associated; + + if ($model instanceof Model) + { + $this->base = $model; + } + else + { + $this->base = new $model; + } + + // Next we'll set the fluent query builder for the relationship and constrain + // the query such that it only returns the models that are appropriate for + // the relationship, typically by setting the foreign key. + $this->table = $this->query(); + + $this->constrain(); + } + + /** + * Get the foreign key name for the given model. + * + * @param string $model + * @param string $foreign + * @return string + */ + public static function foreign($model, $foreign = null) + { + if ( ! is_null($foreign)) return $foreign; + + // If the model is an object, we will simply get the class of the object and + // then take the basename, which is simply the object name minus the + // namespace, and we'll append "_id" to the name. + if (is_object($model)) + { + $model = get_class($model); + } + + return strtolower(basename($model).'_id'); + } + + /** + * Get the foreign key for the relationship. + * + * @return string + */ + protected function foreign_key() + { + return Relationship::foreign($this->base, $this->foreign); + } + +} \ No newline at end of file