<?php namespace System\DB; use System\Str; use System\Config; use System\Inflector; abstract class Eloquent { /** * Indicates if the model exists in the database. * * @var bool */ public $exists = false; /** * The model's attributes. * * Typically, a model has an attribute for each column on the table. * * @var array */ public $attributes = array(); /** * The model's dirty attributes. * * @var array */ public $dirty = array(); /** * The model's ignored attributes. * * Ignored attributes will not be saved to the database, and are * primarily used to hold relationships. * * @var array */ public $ignore = array(); /** * The relationships that should be eagerly loaded. * * @var array */ public $includes = array(); /** * The relationship type the model is currently resolving. * * @var string */ public $relating; /** * The foreign key of the "relating" relationship. * * @var string */ public $relating_key; /** * The table name of the model being resolved. * * This is used during many-to-many eager loading. * * @var string */ public $relating_table; /** * The model query instance. * * @var Query */ public $query; /** * Create a new Eloquent model instance. * * @param array $attributes * @return void */ public function __construct($attributes = array()) { foreach ($attributes as $key => $value) { $this->$key = $value; } } /** * Get the table name for a model. * * @param string $class * @return string */ public static function table($class) { if (property_exists($class, 'table')) { return $class::$table; } return strtolower(Inflector::plural($class)); } /** * Factory for creating new Eloquent model instances. * * @param string $class * @return object */ public static function make($class) { $model = new $class; // Since this method is only used for instantiating/ models for querying // purposes, we will go ahead and set the Query instance on the model. $model->query = Query::table(static::table($class)); return $model; } /** * Create a new model instance and set the relationships * that should be eagerly loaded. * * @return mixed */ public static function with() { $model = static::make(get_called_class()); $model->includes = func_get_args(); return $model; } /** * Get a model by the primary key. * * @param int $id * @return mixed */ public static function find($id) { return static::make(get_called_class())->where('id', '=', $id)->first(); } /** * Get an array of models from the database. * * @return array */ private function _get() { return Eloquent\Hydrator::hydrate($this); } /** * Get the first model result * * @return mixed */ private function _first() { return (count($results = Eloquent\Hydrator::hydrate($this->take(1))) > 0) ? reset($results) : null; } /** * Retrieve the query for a 1:1 relationship. * * @param string $model * @param string $foreign_key * @return mixed */ public function has_one($model, $foreign_key = null) { $this->relating = __FUNCTION__; return $this->has_one_or_many($model, $foreign_key); } /** * Retrieve the query for a 1:* relationship. * * @param string $model * @param string $foreign_key * @return mixed */ public function has_many($model, $foreign_key = null) { $this->relating = __FUNCTION__; return $this->has_one_or_many($model, $foreign_key); } /** * Retrieve the query for a 1:1 or 1:* relationship. * * @param string $model * @param string $foreign_key * @return mixed */ private function has_one_or_many($model, $foreign_key) { // The default foreign key for has one and has many relationships is the name // of the model with an appended _id. For example, the foreign key for a // User model would be user_id. Photo would be photo_id, etc. $this->relating_key = (is_null($foreign_key)) ? strtolower(get_class($this)).'_id' : $foreign_key; return static::make($model)->where($this->relating_key, '=', $this->id); } /** * Retrieve the query for a 1:1 belonging relationship. * * @param string $model * @param string $foreign_key * @return mixed */ public function belongs_to($model, $foreign_key = null) { $this->relating = __FUNCTION__; if ( ! is_null($foreign_key)) { $this->relating_key = $foreign_key; } else { // The default foreign key for belonging relationships is the name of the // relationship method name with _id. So, if a model has a "manager" method // returning a belongs_to relationship, the key would be manager_id. list(, $caller) = debug_backtrace(false); $this->relating_key = $caller['function'].'_id'; } return static::make($model)->where('id', '=', $this->attributes[$this->relating_key]); } /** * Retrieve the query for a *:* relationship. * * @param string $model * @param string $table * @return mixed */ public function has_and_belongs_to_many($model, $table = null) { $this->relating = __FUNCTION__; if ( ! is_null($table)) { $this->relating_table = $table; } else { // By default, the intermediate table name is the plural names of the models // arranged alphabetically and concatenated with an underscore. $models = array(Inflector::plural($model), Inflector::plural(get_class($this))); sort($models); $this->relating_table = strtolower($models[0].'_'.$models[1]); } // The default foreign key for many-to-many relations is the name of the model with an appended _id. // This is the same convention as has_one and has_many. $this->relating_key = strtolower(get_class($this)).'_id'; return static::make($model) ->select(static::table($model).'.*') ->join($this->relating_table, static::table($model).'.id', '=', $this->relating_table.'.'.strtolower($model).'_id') ->where($this->relating_table.'.'.$this->relating_key, '=', $this->id); } /** * Save the model to the database. * * @return bool */ public function save() { if ($this->exists and count($this->dirty) == 0) { return true; } $model = get_class($this); // Since the model was instantiated using "new", a query instance has not been set. $this->query = Query::table(static::table($model)); // Set the creation and update timestamps. if (property_exists($model, 'timestamps') and $model::$timestamps) { $this->updated_at = date('Y-m-d H:i:s'); if ( ! $this->exists) { $this->created_at = $this->updated_at; } } // If the model already exists in the database, we only need to update it. // Otherwise, we'll insert the model into the database. if ($this->exists) { $result = $this->query->where('id', '=', $this->attributes['id'])->update($this->dirty) == 1; } else { $this->attributes['id'] = $this->query->insert_get_id($this->attributes); $result = $this->exists = is_numeric($this->id); } $this->dirty = array(); return $result; } /** * Delete a model from the database. * * @param int $id * @return int */ public function delete($id = null) { if ($this->exists) { return Query::table(static::table(get_class($this)))->delete($this->id); } return 0; } /** * Magic method for retrieving model attributes. */ public function __get($key) { // Check the ignored attributes first. These attributes hold all of the // loaded relationships for the model. if (array_key_exists($key, $this->ignore)) { return $this->ignore[$key]; } // Is the attribute actually a relationship method? If it is, return the // models for the relationship. if (method_exists($this, $key)) { $model = $this->$key(); return ($this->relating == 'has_one' or $this->relating == 'belongs_to') ? $this->ignore[$key] = $model->first() : $this->ignore[$key] = $model->get(); } return (array_key_exists($key, $this->attributes)) ? $this->attributes[$key] : null; } /** * Magic Method for setting model attributes. */ public function __set($key, $value) { // If the key is a relationship, add it to the ignored attributes. if (method_exists($this, $key)) { $this->ignore[$key] = $value; } else { $this->attributes[$key] = $value; $this->dirty[$key] = $value; } } /** * Magic Method for determining if a model attribute is set. */ public function __isset($key) { return (array_key_exists($key, $this->attributes) or array_key_exists($key, $this->ignore)); } /** * Magic Method for unsetting model attributes. */ public function __unset($key) { unset($this->attributes[$key]); unset($this->ignore[$key]); unset($this->dirty[$key]); } /** * Magic Method for handling dynamic method calls. */ public function __call($method, $parameters) { if ($method == 'get') { return $this->_get(); } if ($method == 'first') { return $this->_first(); } if (in_array($method, array('count', 'sum', 'min', 'max', 'avg'))) { return call_user_func_array(array($this->query, $method), $parameters); } // Pass the method to the query instance. This allows the chaining of methods // from the query builder, providing a nice, convenient API. call_user_func_array(array($this->query, $method), $parameters); return $this; } /** * Magic Method for handling dynamic static method calls. */ public static function __callStatic($method, $parameters) { $model = static::make(get_called_class()); if ($method == 'get' or $method == 'all') { return $model->_get(); } if ($method == 'first') { return $model->_first(); } if (in_array($method, array('count', 'sum', 'min', 'max', 'avg'))) { return call_user_func_array(array($model->query, $method), $parameters); } // Pass the method to the query instance. This allows the chaining of methods // from the query builder, providing a nice, convenient API. call_user_func_array(array($model->query, $method), $parameters); return $model; } }