<?php
/**
 * Abstract model class for models built upon our DB table.
 *
 * @author      Iron Bound Designs
 * @since       1.0
 * @copyright   2015 (c) Iron Bound Designs.
 * @license     GPLv2
 */

namespace IronBound\DB;

use Closure;
use IronBound\Cache\Cacheable;
use IronBound\Cache\Cache;
use IronBound\DB\Query\FluentQuery;
use IronBound\DB\Query\Scope;
use IronBound\DB\Relations\HasForeign;
use IronBound\DB\Relations\Relation;
use IronBound\DB\Table\Table;
use IronBound\DB\Table\TimestampedTable;
use IronBound\WPEvents\EventDispatcher;
use IronBound\WPEvents\GenericEvent;

/**
 * Class Model
 *
 * @package IronBound\DB
 *
 * @method static $this create( $attributes ) Create a new instance of this model.
 * @method static creating( callable $listener, int $priority = 10, int $args = 3 ) Listen for the creating event.
 * @method static created( callable $listener, int $priority = 10, int $args = 3 ) Listen for the created event.
 * @method static updating( callable $listener, int $priority = 10, int $args = 3 ) Listen for the updating event.
 * @method static updated( callable $listener, int $priority = 10, int $args = 3 ) Listen for the updated event.
 * @method static saving( callable $listener, int $priority = 10, int $args = 3 ) Listen for the saving event.
 * @method static saved( callable $listener, int $priority = 10, int $args = 3 ) Listen for the saved event.
 * @method static deleting( callable $listener, int $priority = 10, int $args = 3 ) Listen for the deleting event.
 * @method static deleted( callable $listener, int $priority = 10, int $args = 3 ) Listen for the deleted event.
 */
abstract class Model implements Cacheable, \Serializable {

	/// Global Configuration

	/**
	 * @var Manager
	 */
	protected static $_db_manager;

	/**
	 * @var EventDispatcher
	 */
	protected static $_event_dispatcher;

	/**
	 * @var array
	 */
	protected static $_booted = array();

	/// Model Configuration

	/**
	 * Whether all attributes can be mass filled via the constructor
	 * or the magic create() method.
	 *
	 * @var bool
	 */
	protected static $_unguarded = false;

	/**
	 * List of relations to be eager loaded.
	 *
	 * @var array
	 */
	protected static $_eager_load = array();

	/**
	 * Whether to cache this Model's attributes in `WP_Object_Cache`.
	 *
	 * It is recommended to keep this true for most use-cases.
	 *
	 * To disable it, add `protected static $_cache = false;` to your Model subclass.
	 *
	 * @var bool
	 */
	protected static $_cache = true;

	/**
	 * Cache of all relation attribute names.
	 *
	 * @var array
	 */
	protected static $_relation_attribute_cache = array();

	/**
	 * Array of global query scopes for this model. Keyed by their string identifier.
	 *
	 * @var array
	 */
	protected static $_scopes = array();

	/// Instance Configuration

	/**
	 * Storage for the Model's attributes.
	 *
	 * May contain both the "raw" form of an attribute,
	 * i.e. a post ID, or the value form, i.e. a `WP_Post` object.
	 *
	 * @var array
	 */
	private $_attributes = array();

	/**
	 * A snapshot of the model's attributes when it is instantiated.
	 *
	 * Updated whenever the model is saved.
	 *
	 * @var array
	 */
	private $_original = array();

	/**
	 * Whitelist of attributes that are automatically filled.
	 *
	 * @see $_unguarded
	 *
	 * @var array
	 */
	protected $_fillable = array();

	/**
	 * Blacklist of attributes that are not automatically filled.
	 *
	 * @see $_unguarded
	 *
	 * @var array
	 */
	protected $_guarded = array();

	/**
	 * Map of relation name to results.
	 *
	 * @var array
	 */
	protected $_relations = array();

	/**
	 * Whether this model exists in the database.
	 *
	 * @var bool
	 */
	protected $_exists = false;

	/**
	 * Model constructor.
	 *
	 * @since 2.0
	 *
	 * @param array|object $data
	 */
	public function __construct( $data = array() ) {
		$this->init( (object) $data );
	}

	/**
	 * Initialize an object.
	 *
	 * @since 1.0
	 *
	 * @param \stdClass $data
	 */
	protected function init( \stdClass $data ) {

		static::boot_if_not_booted();

		$this->sync_original();
		$this->fill( (array) $data );
	}

	/**
	 * Boot this model if it has not been previously booted.
	 *
	 * Fires two model events, 'booting' and 'booted'.
	 *
	 * @since 2.0
	 */
	protected static function boot_if_not_booted() {

		if ( isset( static::$_booted[ get_called_class() ] ) ) {
			return;
		}

		static::$_booted[ get_called_class() ] = true;

		$instance = new static();

		$instance->fire_model_event( 'booting' );
		static::boot();
		$instance->fire_model_event( 'booted' );
	}

	/**
	 * Boot this model.
	 *
	 * This should perform any one time initialization code.
	 *
	 * @since 2.0
	 */
	protected static function boot() {
		static::boot_traits();
	}

	/**
	 * Boot any traits this model uses.
	 *
	 * This looks for and executes static methods named 'boot_TraitName'.
	 *
	 * @since 2.0
	 */
	protected static function boot_traits() {

		$class = get_called_class();

		// this method will return an empty array on PHP < 5.4
		$uses = class_uses_recursive( get_called_class() );

		foreach ( $uses as $trait ) {
			if ( method_exists( $class, $method = 'boot_' . class_basename( $trait ) ) ) {
				forward_static_call( array( $class, $method ) );
			}
		}
	}

	/**
	 * Fill this model's attributes.
	 *
	 * Filling a model in this way is used to prevent sensitive properties from being mass-assigned.
	 * For example when creating a new model like so: `MyModel::create( $_POST['model'] )`.
	 *
	 * If the model is unguarded, all attributes can be assigned via this method.
	 * Otherwise, the attribute can be filled according to {$_fillable} or {$_guarded}.
	 *
	 * @since   2.0
	 *
	 * @used-by IronBound\DB\Model::__construct()
	 * @used-by IronBound\DB\Model::create()
	 * @used-by IronBound\DB\Model::init()
	 * @used-by IronBound\DB\Query\FluentQuery::update_or_create()
	 *
	 * @param array $data
	 *
	 * @return $this
	 */
	public function fill( array $data = array() ) {

		foreach ( $data as $column => $value ) {

			if ( $this->is_fillable( $column ) ) {
				$this->set_attribute( $column, $value );
			}
		}

		return $this;
	}

	/**
	 * Make a function call with a given attribute guarded.
	 *
	 * @since 2.1.0
	 *
	 * @param string|array $attribute,...
	 * @param callable     $callback
	 */
	public function with_guarded( $attribute, $callback ) {

		if ( is_array( $attribute ) ) {
			$attributes = $attribute;
		} else {
			$attributes = func_get_args();
			$callback   = array_pop( $attributes );
		}

		$unguarded = static::$_unguarded;

		if ( $unguarded ) {
			static::$_unguarded = false;
		}

		$_fillable = $this->_fillable;
		$_guarded  = $this->_guarded;

		if ( $this->_fillable ) {
			$this->_fillable = array_diff( $this->_fillable, $attributes );

			// If the diff removes all fillable attributes we need to use _guarded.
			if ( ! $this->_fillable ) {
				$this->_guarded = $attributes;
			}
		} else {
			$this->_guarded = array_unique( array_merge( $this->_guarded, $attributes ) );
		}

		$callback( $this );

		$this->_fillable = $_fillable;
		$this->_guarded  = $_guarded;

		static::$_unguarded = $unguarded;
	}

	/**
	 * Make a function call with a given attribute unguarded.
	 *
	 * @since 2.1.0
	 *
	 * @param string   $attribute,...
	 * @param callable $callback
	 */
	public function with_unguarded( $attribute, $callback ) {

		if ( static::$_unguarded ) {
			$callback( $this );

			return;
		}

		if ( is_array( $attribute ) ) {
			$attributes = $attribute;
		} else {
			$attributes = func_get_args();
			$callback   = array_pop( $attributes );
		}

		$_fillable = $this->_fillable;
		$_guarded  = $this->_guarded;

		if ( $this->_fillable ) {
			$this->_fillable = array_merge( $this->_fillable, $attributes );
		} else {
			$this->_guarded = array_diff( $this->_guarded, $attributes );
		}

		$callback( $this );

		$this->_fillable = $_fillable;
		$this->_guarded  = $_guarded;

	}

	/**
	 * Determine if a given attribute is fillable.
	 *
	 * @since 2.0
	 *
	 * @param string $column
	 *
	 * @return bool
	 */
	protected function is_fillable( $column ) {

		if ( static::$_unguarded ) {
			return true;
		}

		if ( empty( $this->_fillable ) ) {
			return ! in_array( $column, $this->_guarded );
		} else {
			return in_array( $column, $this->_fillable );
		}
	}

	/**
	 * Set an attribute's value.
	 *
	 * This will call any defined mutators.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Attribute name to update.
	 * @param mixed  $value     New value. In most cases this should be working form of the data.
	 *                          Not the storage version. For example a `WP_Post` object instead of post ID or a
	 *                          `DateTime` object instead of a 'Y-m-d H:i:s' string.
	 *
	 * @return $this
	 *
	 * @throws \OutOfBoundsException If the requested attribute does not exist.
	 */
	public function set_attribute( $attribute, $value ) {

		if ( $this->has_relation( $attribute ) ) {
			if ( is_object( $value ) ) {
				$this->set_relation_value( $attribute, $value );

				return $this;
			}

			unset( $this->_relations[ $attribute ] );
		} elseif ( ! array_key_exists( $attribute, static::table()->get_columns() ) ) {
			throw new \OutOfBoundsException(
				sprintf( "Requested attribute '%s' does not exist for '%s'.", $attribute, get_class( $this ) )
			);
		} elseif ( method_exists( $this, $this->get_mutator_method_for_attribute( $attribute ) ) ) {
			$value = call_user_func( array( $this, $this->get_mutator_method_for_attribute( $attribute ) ), $value );
		}

		$columns = static::table()->get_columns();
		$this->set_raw_attribute( $attribute, $columns[ $attribute ]->prepare_for_storage( $value ) );

		return $this;
	}

	/**
	 * Set an attribute's value in the attributes storage.
	 *
	 * @since   2.0
	 *
	 * @used-by IronBound\DB\Model::set_attribute()
	 *
	 * @param string $attribute Attribute name to update.
	 * @param mixed  $value     New value.
	 *
	 * @return $this
	 */
	public function set_raw_attribute( $attribute, $value ) {

		$attributes = $this->get_raw_attributes();

		$attributes[ $attribute ] = $value;
		$this->set_raw_attributes( $attributes );

		return $this;
	}

	/**
	 * Overwrite all of the attributes for this model.
	 *
	 * Guarding is ignored and mutators are not called.
	 *
	 * @since 2.0
	 *
	 * @see   IronBound\DB\Model::sync_original() for further documentation on the `$sync` parameter.
	 *
	 * @param array $attributes All attributes.
	 * @param bool  $sync       Whether to sync the model so it isn't seen as dirty.
	 *
	 * @return $this
	 */
	public function set_raw_attributes( array $attributes = array(), $sync = false ) {

		$this->_attributes = $attributes;

		if ( $sync ) {
			$this->sync_original();
		}

		return $this;
	}

	/**
	 * Get an attribute's or relation's value.
	 *
	 * If it is an attribute, any accessors will be called.
	 *
	 * If it is a relation, its results will be loaded and returned. The relation's results are cached.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Attribute name.
	 *
	 * @return mixed
	 *
	 * @throws \OutOfBoundsException If the given attribute does not exist.
	 */
	public function get_attribute( $attribute ) {

		if ( $this->has_relation( $attribute ) ) {
			return $this->get_relation_value( $attribute );
		}

		if ( array_key_exists( $attribute, static::table()->get_columns() ) ) {
			return $this->get_attribute_value( $attribute );
		}

		throw new \OutOfBoundsException(
			sprintf( "Requested attribute '%s' does not exist for '%s'.", $attribute, get_class( $this ) )
		);
	}

	/**
	 * Get an attribute's value from storage.
	 *
	 * @since   2.0
	 *
	 * @used-by IronBound\DB\Model::get_attribute()
	 *
	 * @param string $attribute
	 *
	 * @return mixed
	 */
	public function get_raw_attribute( $attribute ) {

		$attributes = $this->get_raw_attributes();

		return isset( $attributes[ $attribute ] ) ? $attributes[ $attribute ] : null;
	}

	/**
	 * Get all of the model's attributes.
	 *
	 * Accessors will not be called. Relations are not included.
	 *
	 * @since 2.0
	 *
	 * @return array
	 */
	public function get_raw_attributes() {
		return $this->_attributes;
	}

	/**
	 * Retrieves an attribute's value and calls the accessor method if it exists.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute
	 *
	 * @return mixed
	 */
	protected function get_attribute_value( $attribute ) {

		$value = $this->get_attribute_from_array( $attribute );

		if ( method_exists( $this, $this->get_accessor_method_for_attribute( $attribute ) ) ) {
			$value = call_user_func( array( $this, $this->get_accessor_method_for_attribute( $attribute ) ), $value );
		}

		return $value;
	}

	/**
	 * Get an attribute's value from the internal attributes array.
	 *
	 * Will preferentially retrieve the value from the attribute value cache,
	 * and update the attribute value cache if the raw value is scalar.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute
	 *
	 * @return mixed|null
	 */
	protected function get_attribute_from_array( $attribute ) {

		$raw     = $this->get_raw_attribute( $attribute );
		$columns = static::table()->get_columns();

		return $columns[ $attribute ]->convert_raw_to_value( $raw );
	}

	/**
	 * Get the accessor method name for an attribute.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute
	 *
	 * @return string
	 */
	protected function get_accessor_method_for_attribute( $attribute ) {
		return "_access_{$attribute}";
	}

	/**
	 * Get the mutator method for an attribute.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute
	 *
	 * @return string
	 */
	protected function get_mutator_method_for_attribute( $attribute ) {
		return "_mutate_{$attribute}";
	}

	/**
	 * Check if a relation exists without loading the relation.
	 *
	 * This is only checking for a unique method name. The return value is not validated
	 * until it is ued by `get_relation()`.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Relation name.
	 *
	 * @return bool
	 */
	protected function has_relation( $attribute ) {
		return method_exists( $this, "_{$attribute}_relation" );
	}

	/**
	 * Get all relations this model has.
	 *
	 * Will be cached on subsequent runs.
	 *
	 * @since 2.0
	 *
	 * @return array
	 */
	public function get_all_relations() {

		if ( isset( static::$_relation_attribute_cache[ get_class( $this ) ] ) ) {
			return static::$_relation_attribute_cache[ get_class( $this ) ];
		}

		$relations = array();
		$methods   = get_class_methods( $this );

		foreach ( $methods as $method ) {
			preg_match( '/^_(\S+)_relation$/', $method, $matches );

			if ( empty( $matches[1] ) ) {
				continue;
			}

			$relations[] = $matches[1];
			$matches     = null;
		}

		static::$_relation_attribute_cache[ get_class( $this ) ] = $relations;

		return $relations;
	}

	/**
	 * Get a relation's controller object.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Relation name.
	 *
	 * @return Relation
	 *
	 * @throws \OutOfBoundsException If no relation exists by the given name.
	 * @throws \UnexpectedValueException If relation method returns an invalid value.
	 */
	public function get_relation( $attribute ) {

		if ( ! $this->has_relation( $attribute ) ) {
			throw new \OutOfBoundsException(
				sprintf( "Requested relation '%s' does not exist for '%s'.", $attribute, get_class( $this ) )
			);
		}

		$method   = "_{$attribute}_relation";
		$relation = $this->{$method}();

		if ( ! $relation instanceof Relation ) {
			throw new \UnexpectedValueException( 'Relation methods must return an IronBound\DB\Relations\Relation object.' );
		}

		return $relation;
	}

	/**
	 * Set the value for a relation.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Relation name.
	 * @param mixed  $value     Relation value.
	 *
	 * @return $this
	 *
	 * @throws \OutOfBoundsException If no relation exists by the given name.
	 */
	public function set_relation_value( $attribute, $value ) {

		if ( ! $this->has_relation( $attribute ) ) {
			throw new \OutOfBoundsException(
				sprintf( "Requested relation '%s' does not exist for '%s'.", $attribute, get_class( $this ) )
			);
		}

		$this->_relations[ $attribute ] = $value;

		return $this;
	}

	/**
	 * Get a relation's value.
	 *
	 * Will load from the cache if possible. Otherwise, will get the relation controller and retrieve and
	 * cache the results.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Relation name.
	 *
	 * @return Collection|Model|mixed
	 */
	protected function get_relation_value( $attribute ) {

		if ( array_key_exists( $attribute, $this->_relations ) ) {
			return $this->_relations[ $attribute ];
		}

		$relation = $this->get_relation( $attribute );
		$this->set_relation_value( $attribute, $relation->get_results() );

		return $this->get_relation_value( $attribute );
	}

	/**
	 * Check if a relation has already been loaded.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute Relation name.
	 *
	 * @return bool
	 */
	public function is_relation_loaded( $attribute ) {
		return array_key_exists( $attribute, $this->_relations );
	}

	/**
	 * Refresh the attributes on this model.
	 *
	 * This will fetch the latest data from either the cache or DB.
	 *
	 * @param bool $destroy_changes If true, will destroy local changes.
	 *
	 * @since 2.0.0
	 */
	public function refresh( $destroy_changes = false ) {

		if ( ! $this->exists() ) {
			return;
		}

		$data = (array) static::get_data_from_pk( $this->get_pk() );

		$this->_original = $data;

		if ( $destroy_changes ) {
			$this->set_raw_attributes( $data );
		}
	}

	/**
	 * Sync the model's original attributes with its current state.
	 *
	 * This will prevent any updates attributes as showing up as dirty.
	 *
	 * @since 2.0
	 *
	 * @return $this
	 */
	public function sync_original() {
		$this->_original = $this->get_raw_attributes();

		return $this;
	}

	/**
	 * Sync an individual attribute's value.
	 *
	 * @since 2.0
	 *
	 * @param string $attribute
	 *
	 * @return $this
	 */
	public function sync_original_attribute( $attribute ) {
		$this->_original[ $attribute ] = $this->get_raw_attribute( $attribute );

		return $this;
	}

	/**
	 * Determine if the model as a whole or given attribute(s) have been modified.
	 *
	 * @since 2.0
	 *
	 * @param array|string...|null $attributes
	 *
	 * @return bool
	 */
	public function is_dirty( $attributes = null ) {

		$dirty = $this->get_dirty();

		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;
	}

	/**
	 * Get the attributes that have been changed since the last sync.
	 *
	 * @since 2.0
	 *
	 * @return array Array of attribute names to changed values.
	 */
	public function get_dirty() {
		$dirty = array();

		foreach ( $this->get_raw_attributes() as $key => $value ) {
			if ( ! array_key_exists( $key, $this->_original ) ) {
				$dirty[ $key ] = $value;
			} elseif ( $value !== $this->_original[ $key ] && ! $this->original_is_numerically_equivalent( $key ) ) {
				$dirty[ $key ] = $value;
			}
		}

		return $dirty;
	}

	/**
	 * Determine if the new and old values for a given key are numerically equivalent.
	 *
	 * {@internal Helper method used for determining which attributes are dirty.}
	 *
	 * @since 2.0
	 *
	 * @param string $key
	 *
	 * @return bool
	 */
	protected function original_is_numerically_equivalent( $key ) {

		$current = $this->get_raw_attribute( $key );

		$original = $this->_original[ $key ];

		return is_numeric( $current ) && is_numeric( $original ) &&
		       strcmp( (string) $current, (string) $original ) === 0;
	}

	/**
	 * Retrieve an instance of this model by its primary key.
	 *
	 * @since 1.0
	 *
	 * @param int|string $pk Primary key of this record.
	 *
	 * @returns static|null
	 *
	 * @throws \InvalidArgumentException If `$pk` is not scalar.
	 */
	public static function get( $pk ) {

		if ( ! $pk ) {
			return null;
		}

		if ( ! is_scalar( $pk ) ) {
			throw new \InvalidArgumentException( 'Primary key must be scalar.' );
		}

		$data = static::get_data_from_pk( $pk );

		if ( $data ) {

			$object = new static( new \stdClass() );
			$object->set_raw_attributes( (array) $data, true );
			$object->_exists = true;

			if ( static::$_cache && ! static::is_data_cached( $pk ) ) {
				Cache::update( $object );
			}

			foreach ( static::$_eager_load as $eager_load ) {
				if ( ! $object->is_relation_loaded( $eager_load ) ) {
					$object->get_relation( $eager_load )->eager_load( array( $object ) );
				}
			}

			return $object;
		} else {
			return null;
		}
	}

	/**
	 * Create a new object from a query builder.
	 *
	 * This is used to bypass guarding when retrieve model data from the database.
	 *
	 * @since 2.0
	 *
	 * @param array $attributes
	 *
	 * @return static
	 */
	public static function from_query( array $attributes = array() ) {

		$instance = new static( new \stdClass() );
		$instance->set_raw_attributes( $attributes, true );
		$instance->_exists = true;

		if ( static::$_cache && ! static::is_data_cached( $instance->get_pk() ) ) {
			Cache::update( $instance );
		}

		return $instance;
	}

	/**
	 * Get data for a primary key.
	 *
	 * @since 1.0
	 *
	 * @param int|string $pk Primary key for this record.
	 *
	 * @return \stdClass|null
	 */
	protected static function get_data_from_pk( $pk ) {

		$data = static::$_cache ? Cache::get( $pk, static::get_cache_group() ) : null;

		if ( ! $data ) {
			$query = new FluentQuery( static::table() );
			$data  = $query->where( static::table()->get_primary_key(), '=', $pk )->first();
		}

		return $data ? (object) $data : null;
	}

	/**
	 * Check if data is cached.
	 *
	 * @since 1.0
	 *
	 * @param int|string $pk Primary key for this record.
	 *
	 * @return bool
	 */
	protected static function is_data_cached( $pk ) {

		if ( ! static::$_cache ) {
			return false;
		}

		$data = Cache::get( $pk, static::get_cache_group() );

		return ! empty( $data );
	}

	/**
	 * Get the table object for this model.
	 *
	 * This must be overwritten by sub-classes.
	 *
	 * @since 1.0
	 *
	 * @returns Table
	 */
	protected static function get_table() {
		// override this in child classes.
		throw new \UnexpectedValueException();
	}

	/**
	 * Update a certain value.
	 *
	 * @deprecated 2.0 This bypasses the attributes layer and as such is deprecated in 2.0.
	 *
	 * @since      1.0
	 *
	 * @param string $key   DB column to update.
	 * @param mixed  $value New value.
	 *
	 * @return bool
	 */
	protected function update( $key, $value ) {

		$columns = static::table()->get_columns();

		$data = array(
			$key => $columns[ $key ]->prepare_for_storage( $value )
		);

		$res = static::make_query_object()->update( $this->get_pk(), $data );

		if ( $res && static::$_cache ) {
			Cache::update( $this );
		}

		return $res;
	}

	/**
	 * Does this model exist in the database..
	 *
	 * @since 2.0
	 *
	 * @return bool
	 */
	public function exists() {
		return $this->_exists;
	}

	/**
	 * Persist this model's changes to the database.
	 *
	 * This will save all savable attributes like WP_Post objects or Models,
	 * and persist any loaded relations.
	 *
	 * The `saving` and `saved` events will be fired. If this model does not yet exist, the `creating` and `created`
	 * events will fire as well. Otherwise, the `updating` and `updated` events will fire.
	 *
	 * @since 2.0
	 *
	 * @param array $options Configure how the saving should be performed.
	 *                       Accepts 'exclude_relations' to allow for given relations to be excluded from persistence.
	 *
	 * @return bool
	 *
	 * @throws Exception
	 */
	public function save( array $options = array() ) {

		if ( isset( $options['exclude_relations'] ) && ! is_array( $options['exclude_relations'] ) ) {
			$options['exclude_relations'] = (array) $options['exclude_relations'];
		}

		$columns = static::table()->get_columns();

		$options = wp_parse_args( $options, array(
			'exclude_relations' => array_filter( $this->get_all_relations(), function ( $attribute ) use ( $columns ) {
				return array_key_exists( $attribute, $columns );
			} )
		) );

		$this->fire_model_event( 'saving' );

		$this->save_has_foreign_relations();

		if ( $this->exists() ) {
			$saved = $this->do_save_as_update();
		} else {
			$saved = $this->do_save_as_insert();
		}

		$this->save_loaded_relations( $options['exclude_relations'] );

		if ( $saved ) {
			$this->finish_save();
		}

		return $saved;
	}

	/**
	 * Save the has foreign relations.
	 *
	 * These need to be saved early because otherwise they will be persisted to both
	 * the attribute value cache, and the relation cache.
	 *
	 * @since 2.0
	 */
	protected function save_has_foreign_relations() {

		foreach ( $this->_relations as $attribute => $value ) {
			$relation = $this->get_relation( $attribute );

			if ( $relation instanceof HasForeign && $value ) {

				$value = $relation->persist( $value );
				$pk    = $relation->get_pk_for_value( $value );

				$this->set_raw_attribute( $attribute, $pk );
				$this->_relations[ $attribute ] = $value;
			}
		}
	}

	/**
	 * Save loaded relations.
	 *
	 * @since 2.0
	 *
	 * @param array $exclude Relations to exclude from being saved.
	 */
	protected function save_loaded_relations( array $exclude = array() ) {
		foreach ( $this->_relations as $relation => $values ) {

			if ( in_array( $relation, $exclude ) ) {
				continue;
			}

			$relation_controller = $this->get_relation( $relation );
			$relation_controller->persist( $values );
		}
	}

	/**
	 * Save model as an insert query.
	 *
	 * @since 2.0
	 *
	 * @return bool
	 *
	 * @throws Exception
	 */
	protected function do_save_as_insert() {

		if ( static::table() instanceof TimestampedTable ) {
			$time = $this->fresh_timestamp();

			$this->set_attribute( static::table()->get_created_at_column(), $time );
			$this->set_attribute( static::table()->get_updated_at_column(), $time );
		}

		$this->fire_model_event( 'creating' );

		$columns = static::table()->get_columns();
		$insert  = array();

		foreach ( $this->get_raw_attributes() as $attribute => $value ) {
			$insert[ $attribute ] = $columns[ $attribute ]->prepare_for_storage( $value );
		}

		$insert_id = static::make_query_object()->insert( $insert );

		if ( $insert_id ) {
			$this->set_raw_attribute( static::table()->get_primary_key(), $insert_id );
		}

		$default_columns_to_fill = array();

		foreach ( static::table()->get_columns() as $name => $column ) {

			if ( ! array_key_exists( $name, $this->get_raw_attributes() ) ) {
				$default_columns_to_fill[] = $name;
			}
		}

		if ( $default_columns_to_fill ) {

			$query          = new FluentQuery( static::table() );
			$default_values = $query->where( static::table()->get_primary_key(), '=', $this->get_pk() )
			                        ->select( $default_columns_to_fill )->first();

			if ( $default_values ) {
				foreach ( $default_values as $column => $value ) {
					$this->set_raw_attribute( $column, $value );
				}
			}
		}

		$this->_exists = true;

		if ( static::$_cache ) {
			Cache::update( $this );
		}

		$this->fire_model_event( 'created' );

		return true;
	}

	/**
	 * Save this model as an update query.
	 *
	 * @since 2.0
	 *
	 * @return bool
	 *
	 * @throws Exception
	 */
	protected function do_save_as_update() {

		$dirty = $this->get_dirty();

		if ( ! $dirty ) {
			return true;
		}

		if ( static::table() instanceof TimestampedTable ) {
			$this->set_attribute( static::table()->get_updated_at_column(), $this->fresh_timestamp() );
			$dirty = $this->get_dirty();
		}

		$columns = static::table()->get_columns();

		$previous = array();
		$update   = array();

		foreach ( $dirty as $column => $value ) {

			if ( array_key_exists( $column, $this->_original ) ) {
				$previous[ $column ] = $this->_original[ $column ];
			}

			$update[ $column ] = $columns[ $column ]->prepare_for_storage( $value );
		}

		$this->fire_model_event( 'updating', array(
			'changed' => $dirty,
			'from'    => $previous
		) );

		$result = static::make_query_object()->update( $this->get_pk(), $update );

		if ( $result ) {

			if ( static::$_cache ) {
				Cache::update( $this );
			}

			$this->fire_model_event( 'updated', array(
				'changed' => $dirty,
				'from'    => $previous
			) );
		}

		return $result;
	}

	/**
	 * Perform cleanup after a save has occurred.
	 *
	 * @since 2.0
	 */
	protected function finish_save() {

		$this->fire_model_event( 'saved' );

		foreach ( $this->_relations as $attribute => $relation ) {
			if ( $relation instanceof Collection ) {
				$relation->clear_memory();
			}
		}

		$this->sync_original();
	}

	/**
	 * Delete this object from the database.
	 *
	 * Will fire `deleting` and `deleted` events.
	 *
	 * @since 1.0
	 *
	 * @throws Exception
	 */
	public function delete() {

		$this->fire_model_event( 'deleting' );

		foreach ( $this->get_all_relations() as $relation ) {
			$this->get_relation( $relation )->on_delete( $this );
		}

		static::make_query_object()->delete( $this->get_pk() );

		if ( static::$_cache ) {
			Cache::delete( $this );
		}

		$this->_exists = false;

		$this->fire_model_event( 'deleted' );
	}

	/**
	 * Create multiple models at once.
	 *
	 * @since 2.0.0
	 *
	 * @param array[]|static[] $to_insert Records to be inserted.
	 *
	 * @return static[]
	 *
	 * @throws \IronBound\DB\Exception
	 */
	public static function create_many( array $to_insert ) {

		$models = array();
		$data   = array();

		foreach ( $to_insert as $model ) {
			$model = $model instanceof static ? $model : new static ( $model );
			$model->fire_model_event( 'saving' );
			$model->fire_model_event( 'creating' );

			$models[] = $model;
			$data[]   = $model->get_raw_attributes();
		}

		$rows = static::make_query_object()->insert_many( $data );

		foreach ( $rows as $i => $row ) {
			$model = $models[ $i ];

			$model->set_raw_attributes( $row );
			$model->fire_model_event( 'created' );
			$model->fire_model_event( 'saved' );
			$model->sync_original();
		}

		return $models;
	}

	/**
	 * Create a fresh UTC timestamp.
	 *
	 * @since 2.0
	 *
	 * @return \DateTime
	 */
	protected function fresh_timestamp() {
		return new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
	}

	/**
	 * Create a new instance of this model.
	 *
	 * @since 2.0
	 *
	 * @param array $attributes
	 *
	 * @return static
	 */
	protected static function _do_create( array $attributes = array() ) {

		$instance = new static( $attributes );
		$instance->save();

		return $instance;
	}

	/**
	 * Make a query object.
	 *
	 * @since 1.2
	 *
	 * @return Query\Simple_Query|null
	 */
	protected static function make_query_object() {
		return static::$_db_manager->make_simple_query_object( static::table()->get_slug() );
	}

	/**
	 * Fire a model event.
	 *
	 * Will bail early if no event dispatcher is available.
	 *
	 * @since 2.0
	 *
	 * @param string $event     Event name.
	 * @param array  $arguments Additional arguments passed to the GenericEvent object.
	 */
	protected function fire_model_event( $event, $arguments = array() ) {

		if ( ! static::$_event_dispatcher ) {
			return;
		}

		$event = static::table()->get_slug() . ".$event";

		static::$_event_dispatcher->dispatch( $event, new GenericEvent( $this, $arguments ) );
	}

	/**
	 * Register a model event listener.
	 *
	 * @since 2.0
	 *
	 * @param string   $event
	 * @param callable $callback
	 * @param int      $priority
	 * @param int      $accepted_args
	 */
	public static function register_model_event( $event, $callback, $priority = 10, $accepted_args = 3 ) {

		if ( isset( static::$_event_dispatcher ) ) {
			$event = static::table()->get_slug() . ".$event";

			static::$_event_dispatcher->add_listener( $event, $callback, $priority, $accepted_args );
		}
	}

	/**
	 * Get the data we'd like to cache.
	 *
	 * This is a bit magical. It iterates through all of the table columns,
	 * and checks if a getter for that method exists. If so, it pulls in that
	 * value. Otherwise, it will pull in the default value. If you'd like to
	 * customize this you should override this function in your child model
	 * class.
	 *
	 * @since 1.0
	 *
	 * @return array
	 */
	public function get_data_to_cache() {

		$data = $this->get_raw_attributes();

		$columns = static::table()->get_columns();

		foreach ( $data as $column => $value ) {
			if ( isset( $columns[ $column ] ) ) {
				$data[ $column ] = $columns[ $column ]->prepare_for_storage( $value );
			}
		}

		return $data;
	}

	/**
	 * Get the cache group for this record.
	 *
	 * By default this returns a string in the following format
	 * "df-{$table_slug}".
	 *
	 * @since 1.0
	 *
	 * @return string
	 */
	public static function get_cache_group() {
		return static::table()->get_slug();
	}

	/**
	 * (PHP 5 >= 5.1.0)<br/>
	 * String representation of object
	 *
	 * @link http://php.net/manual/en/serializable.serialize.php
	 * @return string the string representation of the object or null
	 */
	public function serialize() {
		return serialize( array(
			'pk'       => $this->get_pk(),
			'fillable' => $this->_fillable,
			'original' => $this->_original,
		) );
	}

	/**
	 * (PHP 5 >= 5.1.0)<br/>
	 * Constructs the object
	 *
	 * @link http://php.net/manual/en/serializable.unserialize.php
	 *
	 * @param string $serialized <p>
	 *                           The string representation of the object.
	 *                           </p>
	 *
	 * @return void
	 */
	public function unserialize( $serialized ) {
		$data = unserialize( $serialized );

		$this->init( static::get_data_from_pk( $data['pk'] ) );
		$this->_fillable = $data['fillable'];
		$this->_original = $data['original'];
	}

	/**
	 * Get the table object.
	 *
	 * @since 2.0
	 *
	 * @return Table
	 */
	public static function table() {

		if ( ! $table = static::get_table() ) {
			throw new \UnexpectedValueException( sprintf( '%s::get_table() returned null.', get_called_class() ) );
		}

		return $table;
	}

	/**
	 * Register a global query scope.
	 *
	 * Global query scopes are automatically applied whenever the query() method is called.
	 *
	 * @since 2.0
	 *
	 * @param Scope|string $scope_or_identifier Either a Scope object or an identifier for an accompanying closure.
	 * @param Closure|null $closure             When not using a Scope object, an implementation for the scope.
	 *
	 * @throws \InvalidArgumentException
	 */
	public static function register_global_scope( $scope_or_identifier, Closure $closure = null ) {

		if ( $scope_or_identifier instanceof Scope ) {
			self::$_scopes[ get_called_class() ][ get_class( $scope_or_identifier ) ] = $scope_or_identifier;
		} elseif ( is_string( $scope_or_identifier ) && $closure ) {
			self::$_scopes[ get_called_class() ][ $scope_or_identifier ] = $closure;
		} else {
			throw new \InvalidArgumentException();
		}
	}

	/**
	 * Unregister a global scope.
	 *
	 * @since 2.0
	 *
	 * @param string $identifier
	 */
	public static function unregister_global_scope( $identifier ) {
		unset( static::$_scopes[ get_called_class() ][ $identifier ] );
	}

	/**
	 * Get all registered global scopes for this Model.
	 *
	 * @since 2.0
	 *
	 * @return Scope[]|Closure[]
	 */
	public static function get_global_scopes() {
		return isset( self::$_scopes[ get_called_class() ] ) ? self::$_scopes[ get_called_class() ] : array();
	}

	/**
	 * Create a new query builder without any scopes.
	 *
	 * @since 2.0
	 *
	 * @return FluentQuery
	 */
	public static function query_with_no_global_scopes() {

		static::boot_if_not_booted();

		return FluentQuery::from_model( get_called_class() )->with( static::$_eager_load );
	}

	/**
	 * Create a new query builder without certain global scopes applied.
	 *
	 * @since 2.0
	 *
	 * @param array $scopes
	 *
	 * @return FluentQuery
	 */
	public static function without_global_scopes( array $scopes ) {

		$query = static::query_with_no_global_scopes();

		foreach ( static::get_global_scopes() as $id => $scope ) {

			if ( in_array( $id, $scopes ) ) {
				continue;
			}

			if ( $scope instanceof Scope ) {
				$scope->apply( $query );
			} else {
				$scope( $query );
			}
		}

		return $query;
	}

	/**
	 * Create a new query builder without a given scope.
	 *
	 * @since 2.0
	 *
	 * @param string $scope
	 *
	 * @return FluentQuery
	 */
	public static function without_global_scope( $scope ) {
		return static::without_global_scopes( array( $scope ) );
	}

	/**
	 * Initialize a new query object with the configured eager loaded relations.
	 *
	 * @since 2.0
	 *
	 * @return FluentQuery
	 */
	public static function query() {
		return static::without_global_scopes( array() );
	}

	/**
	 * Retrieve all instances of this model.
	 *
	 * @since 2.0
	 *
	 * @return Collection
	 */
	public static function all() {
		return static::query()->results();
	}

	/**
	 * Perform a query with a given set of relations.
	 *
	 * @since 2.0
	 *
	 * @param $relations,...
	 *
	 * @return FluentQuery
	 */
	public static function with( $relations ) {

		$query = FluentQuery::from_model( get_called_class() );

		call_user_func_array( array( $query, 'with' ), func_get_args() );

		return $query;
	}

	/**
	 * Convert the model to an array.
	 *
	 * @since 2.0
	 *
	 * @return array
	 */
	public function to_array() {

		$attributes = array();

		foreach ( static::table()->get_columns() as $attribute => $column ) {
			$attributes[ $attribute ] = $this->get_attribute( $attribute );
		}

		return $attributes;
	}

	/**
	 * Magic method to retrieve an attribute from the model.
	 *
	 * @since 2.0
	 *
	 * @param string $name
	 *
	 * @return mixed|null
	 *
	 * @throws \OutOfBoundsException If no attribute exists with the given name.
	 */
	public function __get( $name ) {
		return $this->get_attribute( $name );
	}

	/**
	 * Magic method to set an attribute on the model.
	 *
	 * @since 2.0
	 *
	 * @param string $name
	 * @param mixed  $value
	 *
	 * @return void
	 */
	public function __set( $name, $value ) {
		$this->set_attribute( $name, $value );
	}

	/**
	 * Magic method to determine if an attribute exists on the model.
	 *
	 * @since 2.0
	 *
	 * @param string $name
	 *
	 * @return bool
	 */
	public function __isset( $name ) {
		try {
			return $this->get_attribute( $name ) !== null;
		} catch ( \OutOfBoundsException $e ) {
			return false;
		}
	}

	/**
	 * Magic method to remove an attribute from the model.
	 *
	 * @since 2.0
	 *
	 * @param string $name
	 */
	public function __unset( $name ) {
		$this->set_attribute( $name, null );
	}

	/**
	 * Magic method to register event listeners or create a new model.
	 *
	 * @since 2.0
	 *
	 * @param string $name
	 * @param array  $arguments
	 *
	 * @return mixed
	 *
	 * @throws \BadMethodCallException If invalid method name.
	 */
	public static function __callStatic( $name, $arguments ) {

		if ( in_array( $name, array(
			'saved',
			'saving',
			'updated',
			'updating',
			'created',
			'creating',
			'deleted',
			'deleting'
		) ) ) {
			$instance = new static;

			array_unshift( $arguments, $name );

			return call_user_func_array( array( $instance, 'register_model_event' ), $arguments );
		}

		if ( $name === 'create' ) {

			$instance = new static;

			return call_user_func_array( array( $instance, '_do_create' ), $arguments );
		}

		throw new \BadMethodCallException( "__callStatic() failed. No method found for name '$name'." );
	}

	/**
	 * Set the DB Manager to use for this model.
	 *
	 * @since 1.2
	 *
	 * @param Manager $manager
	 */
	public static function set_db_manager( Manager $manager ) {
		static::$_db_manager = $manager;
	}

	/**
	 * Set the EventDispatcher globally.
	 *
	 * @since 2.0
	 *
	 * @param EventDispatcher $dispatcher MUST be configured without a prefix.
	 */
	public static function set_event_dispatcher( EventDispatcher $dispatcher ) {
		static::$_event_dispatcher = $dispatcher;
	}

	/**
	 * Get the EventDispatcher.
	 *
	 * @since 2.0
	 *
	 * @return EventDispatcher
	 */
	public static function get_event_dispatcher() {
		return static::$_event_dispatcher;
	}
}


<?php
/**
 * Where tag.
 *
 * @author      Iron Bound Designs
 * @since       1.0
 * @copyright   2015 (c) Iron Bound Designs.
 * @license     GPLv2
 */

namespace IronBound\DB\Query\Tag;

/**
 * Class Where
 *
 * @package IronBound\DB\Query\Tag
 */
class Where extends Generic {

	/**
	 * And queries.
	 */
	const kAND = 'AND';

	/**
	 * Or queries.
	 */
	const kOR = 'OR';

	/**
	 * Exclusive or queries.
	 */
	const kXOR = 'XOR';

	/**
	 * @var array
	 */
	private $clauses = array();

	/**
	 * @var string
	 */
	private $column;

	/**
	 * @var bool
	 */
	private $operator;

	/**
	 * @var bool
	 */
	private $for_clause = false;

	/**
	 * Constructor.
	 *
	 * @param string      $column
	 * @param string|bool $equality True for =, False for !=
	 * @param mixed       $value    Values should be pre-escaped.
	 */
	public function __construct( $column, $equality, $value ) {
		parent::__construct( 'where' );

		$this->column = $column;

		if ( is_array( $value ) && count( $value ) == 1 ) {
			$this->value = reset( $value );
		} else {
			$this->value = $value;
		}

		if ( is_array( $this->value ) ) {
			$quoted_value = array();

			foreach ( $this->value as $value ) {
				$quoted_value[] = "'$value'";
			}

			$this->value = $quoted_value;
		}

		$this->operator = $equality;
	}

	/**
	 * Generate a Where tag that will be used only as a clause.
	 *
	 * @since 2.0.0
	 *
	 * @return Where
	 */
	public static function for_clause() {
		$where             = new self( '', '', '' );
		$where->for_clause = true;

		return $where;
	}

	/**
	 * Is the where tag empty.
	 *
	 * @since 2.0.0
	 *
	 * @return bool
	 */
	public function is_empty() {
		return ! $this->column && ! $this->clauses;
	}

	/**
	 * Is the Where tag supposed to represent an entire clause.
	 *
	 * @since 2.0.0
	 *
	 * @return bool
	 */
	public function is_for_clause() { return $this->for_clause; }

	/**
	 * Perform an OR.
	 *
	 * @since 1.0
	 *
	 * @param Where $or
	 *
	 * @return $this
	 */
	public function qOr( Where $or ) {
		$this->add_clause( self::kOR, $or );

		return $this;
	}

	/**
	 * Perform an AND.
	 *
	 * @since 1.0
	 *
	 * @param Where $and
	 *
	 * @return $this
	 */
	public function qAnd( Where $and ) {
		$this->add_clause( self::kAND, $and );

		return $this;
	}

	/**
	 * Perform a XOR.
	 *
	 * @since 1.0
	 *
	 * @param Where $xor
	 *
	 * @return $this
	 */
	public function qXor( Where $xor ) {
		$this->add_clause( self::kXOR, $xor );

		return $this;
	}

	/**
	 * Add a clause.
	 *
	 * @since 1.0
	 *
	 * @param       $type
	 * @param Where $clause
	 */
	protected function add_clause( $type, Where $clause ) {
		$this->clauses[] = array(
			'type'   => $type,
			'clause' => $clause
		);
	}

	/**
	 * Get the actual comparison value to be checked.
	 *
	 * @since 1.0
	 *
	 * @return string
	 */
	protected function get_comparison() {

		if ( ! $this->column ) {
			return '';
		}

		$query = "{$this->column} ";

		if ( is_array( $this->value ) ) {
			$values = $this->implode( $this->value );

			if ( $this->operator ) {
				$query .= "IN ($values)";
			} else {
				$query .= "NOT IN ($values)";
			}
		} elseif ( $this->value === null ) {

			if ( $this->operator === true || $this->operator === '=' ) {
				$query .= 'IS NULL';
			} else {
				$query .= 'IS NOT NULL';
			}

		} else {

			if ( is_bool( $this->operator ) ) {
				$operator = $this->operator ? '=' : '!=';
			} else {
				$operator = $this->operator;
			}

			$query .= "$operator '{$this->value}'";
		}

		return $query;
	}

	/**
	 * Override the value method. Handles recursion of nested Where clauses.
	 *
	 * @since 1.0
	 *
	 * @return string
	 */
	protected function get_value() {

		$query = $this->for_clause ? '(' : $this->get_comparison();

		if ( ! empty( $this->clauses ) ) {
			foreach ( $this->clauses as $i => $clause ) {

				// We don't want to add the connector if this is the first clause and there is no comparison.
				if ( strlen( $query ) > 1 ) {
					$query .= ' ' . $clause['type'] . ' ';
				}

				/** @var Where $where */
				$where = $clause['clause'];

				if ( ! $where->is_for_clause() ) {
					$query .= '(';
				}

				$query .= $where->get_value();

				if ( ! $where->is_for_clause() ) {
					$query .= ')';
				}
			}
		}

		if ( $this->for_clause ) {
			$query .= ')';
		}

		return $query;
	}

	/**
	 * @inheritDoc
	 */
	public function __toString() {

		if ( $this->is_empty() ) {
			return '';
		}

		return parent::__toString();
	}
}


<?php
/**
 * Make queries against a custom db table.
 *
 * @author Iron Bound Designs
 * @since  1.0
 */

namespace IronBound\DB\Query;

use IronBound\DB\Exception;
use IronBound\DB\Exception\InvalidColumnException;
use IronBound\DB\Query\Tag\From;
use IronBound\DB\Query\Tag\Select;
use IronBound\DB\Query\Tag\Where;
use IronBound\DB\Table\Table;

/**
 * Class Query
 *
 * @package IronBound\DB
 */
class Simple_Query {

	/**
	 * @var \wpdb
	 */
	protected $wpdb;

	/**
	 * @var Table
	 */
	protected $table;

	/**
	 * Constructor.
	 *
	 * @param \wpdb $wpdb
	 * @param Table $table
	 */
	public function __construct( \wpdb $wpdb, Table $table ) {
		$this->wpdb  = $wpdb;
		$this->table = $table;
	}

	/**
	 * Retrieve a row by the primary key
	 *
	 * @since 1.0
	 *
	 * @param string       $row_key
	 * @param array|string $columns
	 *
	 * @return object
	 *
	 * @throws Exception
	 */
	public function get( $row_key, $columns = '*' ) {
		return $this->get_by( $this->table->get_primary_key(), $row_key, $columns );
	}

	/**
	 * Retrieve a row by a specific column / value
	 *
	 * @since 1.0
	 *
	 * @param string       $column Column name
	 * @param string       $value  Value for the column.
	 * @param string|array $columns
	 *
	 * @return object
	 *
	 * @throws Exception
	 */
	public function get_by( $column, $value, $columns = '*' ) {
		return $this->get_by_or_many_by_helper( $column, $value, $columns, 'get_row' );
	}

	/**
	 * Retrieve a specific column's value by the primary key
	 *
	 * @since 1.0
	 *
	 * @param string $column
	 * @param string $row_key
	 *
	 * @return string
	 *
	 * @throws Exception
	 */
	public function get_column( $column, $row_key ) {
		return $this->get_column_by( $column, $this->table->get_primary_key(), $row_key );
	}

	/**
	 * Retrieve a specific column's value by the the specified column / value
	 *
	 * @since 1.0
	 *
	 * @param string $column Var to retrieve
	 * @param string $where
	 * @param string $value
	 *
	 * @return string
	 *
	 * @throws Exception
	 */
	public function get_column_by( $column, $where, $value ) {

		$builder = new Builder();

		$allowed_columns = $this->table->get_columns();

		if ( ! isset( $allowed_columns[ $column ] ) ) {
			throw new InvalidColumnException( "Invalid column." );
		}

		$builder->append( new Select( "`$column`" ) );
		$builder->append( new From( $this->table->get_table_name( $this->wpdb ) ) );
		$builder->append( new Where( "`$where`", true, $this->escape_value( $where, $value ) ) );

		return $this->wpdb->get_var( trim( $builder->build() ) );
	}

	/**
	 * Get many rows by a certain column.
	 *
	 * @since 2.0
	 *
	 * @param string $column
	 * @param mixed  $value
	 * @param string $columns
	 *
	 * @return array
	 * @throws InvalidColumnException
	 */
	public function get_many_by( $column, $value, $columns = '*' ) {
		return $this->get_by_or_many_by_helper( $column, $value, $columns, 'get_results' );
	}

	/**
	 * Helper function for get_by and get_many_by methods.
	 *
	 * @param string $column
	 * @param mixed  $value
	 * @param string $columns
	 * @param string $method
	 *
	 * @return mixed
	 * @throws InvalidColumnException
	 */
	protected function get_by_or_many_by_helper( $column, $value, $columns = '*', $method ) {

		$builder = new Builder();

		$allowed_columns = $this->table->get_columns();

		if ( is_array( $columns ) ) {

			$select = new Select( null );

			foreach ( $columns as $col ) {

				if ( ! isset( $allowed_columns[ $col ] ) ) {
					throw new InvalidColumnException( "Invalid column." );
				}

				$select->also( "`$col`" );
			}
		} elseif ( $columns == Select::ALL ) {
			$select = new Select( $columns );
		} else {
			if ( ! isset( $allowed_columns[ $columns ] ) ) {
				throw new InvalidColumnException( "Invalid column" );
			}

			$select = new Select( "`$columns`" );
		}

		$builder->append( $select );
		$builder->append( new From( $this->table->get_table_name( $this->wpdb ) ) );
		$builder->append( new Where( "`$column`", true, $this->escape_value( $column, $value ) ) );

		return $this->wpdb->{$method}( trim( $builder->build() ) );
	}

	/**
	 * Retrieve the number of rows matching a certain where clause
	 *
	 * @since 1.0
	 *
	 * @param array $wheres
	 *
	 * @return int
	 */
	public function count( $wheres = array() ) {

		$builder = new Builder();

		$select = new Select( null );
		$select->expression( 'COUNT', '*' );
		$builder->append( $select );

		$builder->append( new From( $this->table->get_table_name( $this->wpdb ) ) );

		if ( ! empty( $wheres ) ) {

			foreach ( $wheres as $column => $value ) {
				if ( ! isset( $where ) ) {
					$where = new Where( "`$column`", true, $this->escape_value( $column, $value ) );
				} else {
					$where->qAnd( new Where( "`$column`", true, $this->escape_value( $column, $value ) ) );
				}
			}

			$builder->append( $where );
		}

		return (int) $this->wpdb->get_var( trim( $builder->build() ) );
	}

	/**
	 * Insert a new row
	 *
	 * @since 1.0
	 *
	 * @param array $data
	 *
	 * @return mixed Insert ID
	 *
	 * @throws Exception
	 */
	public function insert( $data ) {

		// Set default values
		$data = wp_parse_args( $data, $this->table->get_column_defaults() );

		$columns = $this->table->get_columns();

		// White list columns
		$data = array_intersect_key( $data, $columns );

		$null_columns = array();

		foreach ( $data as $col => $val ) {

			if ( $val === null ) {
				$null_columns[] = $col;
			}
		}

		foreach ( $null_columns as $null_column ) {
			unset( $data[ $null_column ] );
		}

		foreach ( $data as $name => $value ) {
			$data[ $name ] = $this->prepare_for_storage( $name, $value );
		}

		$formats = array_fill( 0, count( $data ), '%s' );

		$prev = $this->wpdb->show_errors( false );
		$this->wpdb->insert( $this->table->get_table_name( $this->wpdb ), $data, $formats );
		$this->wpdb->show_errors( $prev );

		if ( $this->wpdb->last_error ) {
			throw $this->generate_exception_from_db_error();
		}

		return $this->wpdb->insert_id;
	}

	/**
	 * Insert multiple rows at once.
	 *
	 * @since 2.0.0
	 *
	 * @param array $rows Array of array of fields to values.
	 *
	 * @return array
	 *
	 * @throws \IronBound\DB\Exception
	 */
	public function insert_many( array $rows ) {

		$pk = $this->table->get_primary_key();
		$tn = $this->table->get_table_name( $this->wpdb );

		$columns         = $this->table->get_columns();
		$auto_increments = strpos( $columns[ $pk ], 'auto_increment' ) !== false;

		/**
		 * Filter whether an insert_many API call should perform a single INSERT request.
		 *
		 * If this is disabled, mysql transactions must be supported.
		 *
		 * @since 2.0.0
		 *
		 * @param bool
		 */
		if ( ! apply_filters( 'ironbound_db_perform_insert_many_as_single_query', true ) ) {

			$this->wpdb->query( 'START TRANSACTION;' );

			try {
				foreach ( $rows as $i => $row ) {
					$id = $this->insert( $row );

					if ( $id ) {
						$rows[ $i ][ $pk ] = $id;
					}
				}
			} catch ( Exception $e ) {
				$this->wpdb->query( 'ROLLBACK;' );

				throw $e;
			}

			$this->wpdb->query( 'COMMIT;' );

			return $rows;
		}

		$defaults = $this->table->get_column_defaults();

		if ( $auto_increments ) {
			unset( $defaults[ $pk ] );
		}

		ksort( $defaults );
		$fields = '`' . implode( '`, `', array_keys( $defaults ) ) . '`';

		$formats = '';
		$values  = array();

		foreach ( $rows as $i => $row ) {

			$data   = array();
			$format = array_fill( 0, count( $defaults ), '%s' );

			$k = 0;
			foreach ( $defaults as $column => $default ) {
				$value = array_key_exists( $column, $row ) ? $row[ $column ] : $default;

				if ( $value === null ) {
					$format[ $k ] = 'NULL';
				} else {
					$data[ $column ] = $value;
				}

				$k ++;
			}

			$values = array_merge( $values, array_values( $data ) );
			$formats .= '(' . implode( ',', $format ) . '),';
		}

		$formats = substr( $formats, 0, - 1 );
		$sql     = "INSERT INTO `{$tn}` ({$fields}) VALUES {$formats};";

		$prev = $this->wpdb->show_errors( false );
		$this->wpdb->query( $this->wpdb->prepare( $sql, $values ) );
		$this->wpdb->show_errors( $prev );

		if ( $this->wpdb->last_error ) {
			throw $this->generate_exception_from_db_error();
		}

		$first_id = $this->wpdb->insert_id;

		if ( ! $first_id ) {
			return $rows;
		}

		$id = $first_id;

		foreach ( $rows as $i => $row ) {
			$rows[ $i ][ $pk ] = $id;
			$id ++;
		}

		return $rows;
	}

	/**
	 * Update a row
	 *
	 * @since 1.0
	 *
	 * @param string $row_key
	 * @param array  $data
	 * @param array  $where
	 *
	 * @return  bool
	 *
	 * @throws Exception
	 */
	public function update( $row_key, $data, $where = array() ) {

		if ( empty( $row_key ) ) {
			return false;
		}

		if ( empty( $where ) ) {
			$where = array( $this->table->get_primary_key() => $row_key );
		}

		$columns = $this->table->get_columns();

		// White list columns
		$data = array_intersect_key( $data, $columns );

		foreach ( $data as $name => $value ) {
			$data[ $name ] = $this->prepare_for_storage( $name, $value );
		}

		$formats      = array_fill( 0, count( $data ), '%s' );
		$where_format = array_fill( 0, count( $where ), '%s' );

		$prev   = $this->wpdb->show_errors( false );
		$result = $this->wpdb->update( $this->table->get_table_name( $this->wpdb ), $data, $where, $formats, $where_format );
		$this->wpdb->show_errors( $prev );

		if ( $this->wpdb->last_error ) {
			throw $this->generate_exception_from_db_error();
		}

		return (bool) $result;
	}

	/**
	 * Delete a row identified by the primary key
	 *
	 * @since 1.0
	 *
	 * @param string $row_key
	 *
	 * @return  bool
	 *
	 * @throws Exception
	 */
	public function delete( $row_key ) {

		if ( empty( $row_key ) ) {
			return false;
		}

		$row_key = $this->escape_value( $this->table->get_primary_key(), $row_key );

		$prev   = $this->wpdb->show_errors( false );
		$result = $this->wpdb->delete( $this->table->get_table_name( $this->wpdb ), array(
			$this->table->get_primary_key() => $row_key
		), '%s' );
		$this->wpdb->show_errors( $prev );

		if ( $this->wpdb->last_error ) {
			throw $this->generate_exception_from_db_error();
		}

		return (bool) $result;
	}

	/**
	 * Delete many rows.
	 *
	 * @since 1.0
	 *
	 * @param $wheres array
	 *
	 * @return bool
	 *
	 * @throws Exception
	 */
	public function delete_many( $wheres ) {

		$format = array_fill( 0, count( $wheres ), '%s' );
		$prev   = $this->wpdb->show_errors( false );
		$result = $this->wpdb->delete( $this->table->get_table_name( $this->wpdb ), $wheres, $format );
		$this->wpdb->show_errors( $prev );

		if ( $this->wpdb->last_error ) {
			throw $this->generate_exception_from_db_error();
		}

		return (bool) $result;
	}

	/**
	 * Escape a value using sprintf.
	 *
	 * @param string $column
	 * @param mixed  $value
	 *
	 * @return mixed
	 *
	 * @throws Exception
	 */
	public function escape_value( $column, $value ) {
		return esc_sql( $this->prepare_for_storage( $column, $value ) );
	}

	/**
	 * Prepare a value for storage.
	 *
	 * @since 2.0
	 *
	 * @param string $column
	 * @param mixed  $value
	 *
	 * @return mixed|string
	 * @throws InvalidColumnException
	 */
	public function prepare_for_storage( $column, $value ) {

		$columns = $this->table->get_columns();

		if ( ! isset( $columns[ $column ] ) ) {
			throw new InvalidColumnException( "Invalid database column '$column'." );
		}

		if ( is_null( $value ) ) {
			return null;
		}

		if ( empty( $value ) ) {
			return '';
		}

		return $columns[ $column ]->prepare_for_storage( $value );
	}

	/**
	 * Generate an Exception object from a DB error.
	 *
	 * @return Exception
	 */
	protected function generate_exception_from_db_error() {

		if ( ! $this->wpdb->last_error ) {
			return null;
		}

		if ( $this->is_mysqli() ) {
			$error_number = mysqli_errno( $this->get_dbh() );
		} else {
			$error_number = mysql_errno( $this->get_dbh() );
		}

		return new Exception( $this->wpdb->last_error, $error_number );
	}

	/**
	 * Check if wpdb is using mysqli
	 *
	 * @return bool
	 */
	protected function is_mysqli() {
		return $this->wpdb->use_mysqli;
	}

	/**
	 * Get the mysql dbh
	 *
	 * @return mixed
	 */
	protected function get_dbh() {
		return $this->wpdb->dbh;
	}
}


<?php
/**
 * Contains the TermSaver class definition.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Saver;

/**
 * Class TermSaver
 *
 * @package IronBound\DB\Value
 */
class TermSaver extends Saver {

	/**
	 * @inheritDoc
	 */
	public function get_pk( $value ) {
		return $value->term_id;
	}

	/**
	 * @inheritDoc
	 */
	public function get_model( $pk ) {

		if ( ! $pk ) {
			return null;
		}

		$term = get_term( $pk );

		if ( is_wp_error( $term ) ) {
			throw new \UnexpectedValueException( $term->get_error_message() );
		}

		return $term;
	}

	/**
	 * @inheritDoc
	 */
	public function make_model( $attributes ) {
		return new \WP_Term( (object) $attributes );
	}

	/**
	 * @inheritDoc
	 */
	public function save( $value, array $options = array() ) {

		if ( ! $value instanceof \WP_Term && ! property_exists( $value, 'term_id' ) ) {
			throw new \InvalidArgumentException( 'ForeignTerm can only save WP_Term objects.' );
		}

		if ( ! $value->term_id ) {
			return $this->do_save( $value );
		}

		$current = get_term( $value->term_id, $value->taxonomy );

		if ( ! $current ) {
			return $this->do_save( $value );
		}

		$old = $current->to_array();
		$new = $value->to_array();

		if ( $this->has_changes( $old, $new ) ) {
			return $this->do_save( $value );
		}

		return $value;
	}

	/**
	 * Do the saving for a term.
	 *
	 * @since 2.0
	 *
	 * @param \WP_Term $term
	 *
	 * @return \WP_Term
	 */
	protected function do_save( $term ) {

		if ( ! $term->term_id ) {
			$ids = wp_insert_term( wp_slash( $term->name ), $term->taxonomy, array(
				'description' => wp_slash( $term->description ),
				'parent'      => $term->parent,
				'slug'        => $term->slug
			) );
		} else {
			$ids = wp_update_term( $term->term_id, $term->taxonomy, array(
				'name'        => wp_slash( $term->name ),
				'description' => wp_slash( $term->description ),
				'parent'      => $term->parent,
				'slug'        => $term->slug
			) );
		}

		if ( is_wp_error( $ids ) ) {
			throw new \InvalidArgumentException( 'Error encountered while saving WP_Term: ' . $ids->get_error_message() );
		}

		$term = get_term( $ids['term_id'], $term->taxonomy );

		if ( is_wp_error( $term ) ) {
			throw new \UnexpectedValueException( $term->get_error_message() );
		}

		return $term;
	}
}


<?php
/**
 * HasForeign class definition.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Relations;

use IronBound\DB\Collection;
use IronBound\DB\Model;
use IronBound\DB\Query\FluentQuery;
use IronBound\DB\Saver\ModelSaver;
use IronBound\DB\Saver\Saver;

/**
 * Class HasForeign
 *
 * @package IronBound\DB\Relations
 */
class HasForeign extends Relation {

	/**
	 * @var string
	 */
	protected $related_primary_key_column;

	/**
	 * HasForeign constructor.
	 *
	 * @param string       $attribute
	 * @param Model        $parent
	 * @param string|Saver $related
	 */
	public function __construct( $attribute, Model $parent, $related ) {

		if ( $related instanceof Saver ) {
			$saver   = $related;
			$related = '';
		} else {
			$saver = new ModelSaver( $related );
		}

		if ( $related ) {
			$this->related_primary_key_column = $related::table()->get_primary_key();
		}

		parent::__construct( $related, $parent, $attribute, $saver );

		$this->cache( false );
	}

	/**
	 * Make a FluentQuery object.
	 *
	 * @since 2.0
	 *
	 * @param bool $model_class Set the Model class if possible.
	 *
	 * @return FluentQuery
	 */
	protected function make_query_object( $model_class = false ) {

		$query = call_user_func( array( $this->related_model, 'query_with_no_global_scopes' ) );

		if ( ! $model_class ) {
			$query->set_model_class( null );
		}

		return $query;
	}

	/**
	 * @inheritDoc
	 */
	protected function fetch_results() {

		if ( $this->is_attribute_valid_model() ) {
			return $this->parent->get_raw_attribute( $this->attribute );
		}

		return $this->saver->get_model( $this->parent->get_raw_attribute( $this->attribute ) );
	}

	/**
	 * Is the stored attribute a valid model for this relation.
	 *
	 * @return bool
	 */
	protected function is_attribute_valid_model() {
		return $this->parent->get_raw_attribute( $this->attribute ) instanceof $this->related_model;
	}

	/**
	 * Fetch results for an eager loading query.
	 *
	 * @since 2.0
	 *
	 * @param array $primary_keys
	 *
	 * @return \Doctrine\Common\Collections\Collection|\IronBound\DB\Collection
	 */
	protected function fetch_results_for_eager_load( $primary_keys ) {

		if ( ! $primary_keys ) {
			return new Collection( array(), false, $this->saver );
		}

		$query = $this->make_query_object( true );
		$query->where( $this->related_primary_key_column, true, $primary_keys );

		return $query->results( $this->saver );
	}

	/**
	 * @inheritDoc
	 */
	public function eager_load( array $models, $callback = null ) {

		/** @var Model[] $map */
		$map = array();
		$pks = array();

		foreach ( $models as $model ) {

			$pk = $model->get_raw_attribute( $this->attribute );

			// while freshly hydrated models will never have objects as the raw attribute value
			// this is a public API method so let's add safe guards just in case.
			if ( is_object( $pk ) ) {
				$pk = $this->saver->get_pk( $pk );
			}

			if ( ! $pk ) {
				continue;
			}

			$pks[]        = $pk;
			$map[ $pk ][] = $model;
		}

		$related_models = $this->fetch_results_for_eager_load( $pks );

		foreach ( $related_models as $related_model ) {
			if ( ! empty( $map[ $this->saver->get_pk( $related_model ) ] ) ) {
				foreach ( $map[ $this->saver->get_pk( $related_model ) ] as $mapped ) {
					$mapped->set_raw_attribute( $this->attribute, $related_model );
					$mapped->sync_original_attribute( $this->attribute );
				}
			}
		}

		return $related_models;
	}

	/**
	 * @inheritDoc
	 */
	public function persist( $values ) {
		return $this->saver->save( $values );
	}

	/**
	 * Get the primary key for the related model.
	 *
	 * @since 2.0
	 *
	 * @param object $value
	 *
	 * @return int|string
	 */
	public function get_pk_for_value( $value ) {
		return $this->saver->get_pk( $value );
	}
}


<?php
/**
 * This file is part of PicORM.
 *
 * PicORM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PicORM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PicORM.  If not, see <http://www.gnu.org/licenses/>.
 *
 * PHP version 5.4
 *
 * @category Query
 * @package  PicORM
 * @author   iNem0o <contact@inem0o.fr>
 * @license  LGPL http://opensource.org/licenses/lgpl-license.php
 * @link     https://github.com/iNem0o/PicORM
 */

namespace PicORM;

/**
 * Class QueryBuilder
 * Allow you to easily build MySQL query using PHP Object
 *
 * @category Query
 * @package  PicORM
 * @author   iNem0o <contact@inem0o.fr>
 * @license  LGPL http://opensource.org/licenses/lgpl-license.php
 * @link     https://github.com/iNem0o/PicORM
 */
class QueryBuilder
{
    /**
     * Fields names (UPDATE)
     *
     * @var array
     */
    protected $_setName = array();

    /**
     * Fields values (UPDATE)
     *
     * @var array
     */
    protected $_setVal = array();

    /**
     * Insert VALUES (UPDATE or INSERT)
     *
     * @var array
     */
    protected $_insertValues = array();

    /**
     * Temp insert values
     *
     * @var array
     */
    protected $_lastInsertValues = array();

    /**
     * Marker for SELECT query type
     */
    const SELECT = 1;

    /**
     * Marker for DELETE query type
     */
    const DELETE = 2;

    /**
     * Marker for UPDATE query type
     */
    const UPDATE = 3;

    /**
     * Marker for INSERT query type
     */
    const INSERT = 4;

    /**
     * Query TYPE (UPDATE|INSERT|DELETE|SELECT)
     *
     * @var
     */
    protected $_queryType;

    /**
     * Query hint (ex : SQL_NO_CACHE)
     * @var array
     */
    protected $_queryHint = array();

    /**
     * Fields to select
     * @var array
     */
    protected $_select = array();

    /**
     * Table name to insert
     * @var string
     */
    protected $_insert = '';

    /**
     * Table name to update
     * @var string
     */
    protected $_update = '';

    /**
     * Table name to delete from
     * @var string
     */
    protected $_delete = '';

    /**
     * Tables to select from
     * @var string
     */
    protected $_from = '';

    /**
     * Join definition
     * @var array
     */
    protected $_join = array();

    /**
     * Where criteria
     * @var array
     */
    protected $_where = array();

    /**
     * Order by criteria
     * @var array
     */
    protected $_orderBy = array();

    /**
     * Group by criteria
     * @var array
     */
    protected $_groupBy = array();

    /**
     * Having criteria
     * @var string
     */
    protected $_having = '';

    /**
     * Limit criteria
     * @var string
     */
    protected $_limit = '';

    /**
     * Build query string from setted data
     *
     * @return string
     * @throws Exception
     */
    public function buildQuery()
    {
        $where   = implode(' ', $this->_where);
        $join    = implode(' ', $this->_join);
        $orderBy = count($this->_orderBy) > 0 ? sprintf('ORDER BY %s', implode(',', $this->_orderBy)) : '';
        $groupBy = count($this->_groupBy) > 0 ? sprintf('GROUP BY %s', implode(',', $this->_groupBy)) : '';
        $hint    = count($this->_queryHint) > 0 ? sprintf(' %s ', implode(',', $this->_queryHint)) : '';
        $limit   = $this->_limit;
        $query   = '';

        switch ($this->_queryType) {
            case self::SELECT:
                $select = implode(',', $this->_select);
                $from   = !empty($this->_from) ? sprintf('FROM %s', $this->_from) : '';
                $query  = sprintf(
                    "SELECT %s %s %s %s %s %s %s %s %s",
                    $hint,
                    $select,
                    $from,
                    $join,
                    $where,
                    $groupBy,
                    $this->_having,
                    $orderBy,
                    $limit
                );
                break;
            case self::INSERT:
                $values = '';

                // save lastInsertValues
                if (!empty($this->_lastInsertValues)) {
                    $this->_insertValues[]   = $this->_lastInsertValues;
                    $this->_lastInsertValues = array();
                }

                // grab field name from first line
                $keys   = array_keys($this->_insertValues[0]);
                $nbKeys = count($keys) - 1;

                // reorder values and build string
                foreach ($this->_insertValues as $k => $oneVal) {
                    if ($k > 0) {
                        $values .= ',';
                    }
                    $values .= '(';
                    for ($i = 0; $i <= $nbKeys; $i++) {
                        $values .= $oneVal[$keys[$i]];
                        if ($i != $nbKeys) {
                            $values .= ',';
                        }
                    }
                    $values .= ')';
                }

                // build insert query string
                $query = sprintf(
                    "INSERT INTO %s (%s) VALUES %s",
                    $this->_insert,
                    implode(',', $keys),
                    $values
                );
                break;
            case self::UPDATE:
                $strSet = '';

                // test if params number match the values
                if (count($this->_setName) != count($this->_setVal)) {
                    throw new Exception("number of params / values is not correct");
                }

                // build SET values
                $maxParams = count($this->_setName) - 1;
                for ($i = 0; $i <= $maxParams; $i++) {
                    $strSet .= sprintf('%s = %s', $this->_setName[$i], $this->_setVal[$i]);
                    if ($i !== $maxParams) {
                        $strSet .= ',';
                    }
                }

                // build update query string
                $query = sprintf(
                    "UPDATE %s %s SET %s %s %s %s",
                    $this->_update,
                    $join,
                    $strSet,
                    $where,
                    $orderBy,
                    $limit
                );
                break;
            case self::DELETE:

                // build delete query string
                $query = sprintf(
                    "DELETE %s FROM %s %s %s %s %s",
                    $this->_delete,
                    $this->_delete,
                    $join,
                    $where,
                    $orderBy,
                    $limit
                );
                break;
        }

        return trim($query);
    }

    /**
     * Add a query hint modifier (ex SQL_NO_CACHE)
     *
     * @param $queryHint
     */
    public function queryHint($queryHint)
    {
        $this->_queryHint[] = $queryHint;

        return $this;
    }

    /**
     * Add values (INSERT|UPDATE)
     *
     * @param string $nameParams - field name
     * @param string $val        - field value
     *
     * @return $this
     */
    public function values($nameParams, $val)
    {
        $this->_lastInsertValues[$nameParams] = $val;

        return $this;
    }

    /**
     * Create new values set for multiple insert query
     *
     * @param $nameParams - field name
     * @param $val        - field value
     *
     * @return $this
     */
    public function newValues($nameParams, $val)
    {
        // store value buffer data
        if (count($this->_lastInsertValues) > 0) {
            $this->_insertValues[] = $this->_lastInsertValues;
        }

        // set new value in insert buffer
        $this->_lastInsertValues = array($nameParams => $val);

        return $this;
    }

    /**
     * Set values for update query
     *
     * @alias values
     *
     * @param $nameParams
     * @param $val
     *
     * @return $this
     */
    public function set($nameParams, $val)
    {
        $this->_setName[] = $nameParams;
        $this->_setVal[]  = $val;

        return $this;
    }

    /**
     * Add field to select
     *
     * @param $field
     *
     * @return $this
     */
    public function select($field)
    {
        $this->_queryType = self :: SELECT;
        if (!is_array($field)) {
            $field = array($field);
        }
        $this->_select = array_merge($this->_select, $field);

        return $this;
    }

    /**
     * Reset select and add a new field
     *
     * @param $field
     *
     * @return $this
     */
    public function resetSelect($field = null)
    {
        $this->_select = array();
        if ($field !== null) {
            $this->select($field);
        }

        return $this;
    }

    /**
     * Configure object for UPDATE query
     * and set the main table name
     *
     * @param $tableName
     *
     * @return $this
     */
    public function update($tableName)
    {
        $this->_queryType = self :: UPDATE;
        $this->_update    = $tableName;

        return $this;
    }

    /**
     * Configure object for DELETE query
     * and set the main table name
     *
     * @param $tableName
     *
     * @return $this
     */
    public function delete($tableName)
    {
        $this->_queryType = self :: DELETE;
        $this->_delete    = $tableName;

        return $this;
    }

    /**
     * Configure object for INSERT query
     * and set the main table name
     *
     * @param $tableName
     *
     * @return $this
     */
    public function insertInto($tableName)
    {
        $this->_queryType = self :: INSERT;
        $this->_insert    = $tableName;

        return $this;
    }

    /**
     * Add FROM clause with table name and optional alias
     *
     * @param      $tableName
     * @param null $aliasTable
     *
     * @return $this
     */
    public function from($tableName, $aliasTable = null)
    {
        $this->_from = sprintf('%s %s', $tableName, $aliasTable);

        return $this;
    }

    /**
     * Add JOIN clause
     *
     * @param $mode     string    INNER|LEFT|RIGHT or other mysql join possibilities
     * @param $table    string    table name
     * @param $on       string    join condition
     *
     * @return $this
     */
    public function join($mode, $table, $on)
    {
        $this->_join[] = sprintf('%s JOIN %s ON %s', $mode, $table, $on);

        return $this;
    }

    /**
     * Add INNER JOIN clause
     *
     * @param $table    string    table name
     * @param $on       string    join condition
     *
     * @return $this
     */
    public function innerJoin($table, $on)
    {
        $this->join('INNER', $table, $on);

        return $this;
    }

    /**
     * Add LEFT JOIN clause
     *
     * @param $table    string    table name
     * @param $on       string    join condition
     *
     * @return $this
     */
    public function leftJoin($table, $on)
    {
        $this->join('LEFT', $table, $on);

        return $this;
    }

    /**
     * Adding WHERE clause (default with AND)
     *
     * @param        $field
     * @param        $comparisonOperator
     * @param        $value
     * @param string $booleanOperator (AND|OR)
     *
     * @return $this
     */
    public function where($field, $comparisonOperator, $value, $booleanOperator = 'AND')
    {
        // first where value, need to override operator
        if (count($this->_where) == 0) {
            $booleanOperator = "WHERE";
        }

        // create and store where
        $this->_where[] = sprintf("%s %s %s %s", $booleanOperator, $field, $comparisonOperator, $value);

        return $this;
    }

    /**
     * Alias to add WHERE clause with AND operator
     *
     * @param $field
     * @param $comparisonOperator
     * @param $value
     *
     * @return $this
     */
    public function andWhere($field, $comparisonOperator, $value)
    {
        $this->where($field, $comparisonOperator, $value, 'AND');

        return $this;
    }

    /**
     * Alias to add WHERE clause with OR
     *
     * @param $field
     * @param $comparisonOperator
     * @param $value
     *
     * @return $this
     */
    public function orWhere($field, $comparisonOperator, $value)
    {
        $this->where($field, $comparisonOperator, $value, 'OR');

        return $this;
    }

    /**
     * Add data to ORDER BY clause
     *
     * @param        $orderName
     * @param string $orderVal
     *
     * @return $this
     */
    public function orderBy($orderName, $orderVal = '')
    {
        $this->_orderBy[] = sprintf("%s %s", $orderName, $orderVal);

        return $this;
    }

    /**
     * Reset the order clause from query
     *
     * @param null   $orderName
     * @param string $orderVal
     *
     * @return $this
     */
    public function resetOrderBy($orderName = null, $orderVal = '')
    {
        $this->_orderBy = array();
        if ($orderName !== null) {
            $this->orderBy($orderName, $orderVal);
        }

        return $this;
    }

    /**
     * Add a GROUP BY clause to the query
     *
     * @param $groupByField
     *
     * @return $this
     */
    public function groupBy($groupByField)
    {
        $this->_groupBy[] = sprintf("%s", $groupByField);

        return $this;
    }

    /**
     * Add a HAVING clause to the query
     *
     * @param $having
     *
     * @return $this
     */
    public function having($having)
    {
        $this->_having = sprintf('HAVING %s', $having);

        return $this;
    }

    /**
     * Set LIMIT clause
     *
     * @param      $limitStart
     * @param null $limitEnd
     *
     * @return $this
     */
    public function limit($limitStart = null, $limitEnd = null)
    {
        if ($limitStart === null) {
            return $this;
        }

        if ($limitEnd === null) {
            $this->_limit = sprintf('LIMIT %d', $limitStart);
        } else {
            $this->_limit = sprintf('LIMIT %d, %d', $limitStart, $limitEnd);
        }

        return $this;
    }

    /**
     * Delete the limit clause from query
     *
     * @param null $limitStart
     * @param null $limitEnd
     *
     * @return $this
     */
    public function resetLimit($limitStart = null, $limitEnd = null)
    {
        $this->_limit = '';
        if ($limitStart !== null) {
            $this->limit($limitStart, $limitEnd);
        }

        return $this;
    }

    /**
     * Return all fields values set during building an UPDATE query
     *
     * @return array
     */
    public function getUpdateValues()
    {
        return $this->_setVal;
    }
}


<?php
/**
 * TrashSupport trait.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Extensions\Trash;

use IronBound\DB\Query\FluentQuery;
use IronBound\DB\Extensions\Trash\TrashTable;

/**
 * Trait TrashSupport
 * @package IronBound\DB\Model
 */
trait TrashSupport {

	/**
	 * @var bool
	 */
	private $force_deleting = false;

	/**
	 * Initialize the TrashSupport trait.
	 *
	 * @since 2.0
	 */
	public static function boot_TrashSupport() {

		$table = static::table();

		if ( ! $table instanceof TrashTable ) {
			throw new \UnexpectedValueException( sprintf( "%s model's table must implement TrashTable." ), get_called_class() );
		}

		static::register_global_scope( 'trash', function ( FluentQuery $query ) use ( $table ) {
			$query->and_where( $table->get_deleted_at_column(), true, null );
		} );
	}

	/**
	 * @inheritdoc
	 */
	public function delete() {

		if ( $this->force_deleting ) {
			return parent::delete();
		} else {

			$this->fire_model_event( 'trashing' );

			$table = static::table();

			$this->{$table->get_deleted_at_column()} = $this->fresh_timestamp();

			$this->fire_model_event( 'trashed' );

			return $this->save();
		}
	}

	/**
	 * @inheritdoc
	 */
	public function force_delete() {
		$this->force_deleting = true;
		$this->delete();
		$this->force_deleting = false;
	}

	/**
	 * @inheritdoc
	 */
	public function untrash() {

		$this->fire_model_event( 'untrashing' );

		$table                                   = static::table();
		$this->{$table->get_deleted_at_column()} = null;

		$this->fire_model_event( 'untrashed' );

		return $this->save();
	}

	/**
	 * @inheritdoc
	 */
	public function is_trashed() {

		$table  = static::table();
		$column = $table->get_deleted_at_column();

		return $this->{$column} !== null;
	}

	/**
	 * @inheritdoc
	 */
	public static function with_trashed() {
		return static::without_global_scope( 'trash' );
	}

	/**
	 * @inheritdoc
	 */
	public static function only_trashed() {

		/** @var FluentQuery $query */
		$query = static::with_trashed();

		$query->where( static::table()->get_deleted_at_column(), false, null );

		return $query;
	}

	/**
	 * @inheritdoc
	 */
	public static function trashing( $callback, $priority = 10, $accepted_args = 3 ) {
		return static::register_model_event( 'trashing', $callback, $priority, $accepted_args );
	}

	/**
	 * @inheritdoc
	 */
	public static function trashed( $callback, $priority = 10, $accepted_args = 3 ) {
		return static::register_model_event( 'trashed', $callback, $priority, $accepted_args );
	}

	/**
	 * @inheritdoc
	 */
	public static function untrashing( $callback, $priority = 10, $accepted_args = 3 ) {
		return static::register_model_event( 'untrashing', $callback, $priority, $accepted_args );
	}

	/**
	 * @inheritdoc
	 */
	public static function untrashed( $callback, $priority = 10, $accepted_args = 3 ) {
		return static::register_model_event( 'untrashed', $callback, $priority, $accepted_args );
	}
}


<?php
/**
 * Contains the abstract Relation class.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Relations;

use IronBound\DB\Collection;
use IronBound\DB\Model;
use IronBound\DB\Saver\ModelSaver;
use IronBound\DB\Saver\Saver;
use IronBound\WPEvents\GenericEvent;

/**
 * Class Relation
 * @package IronBound\DB\Relations
 */
abstract class Relation {

	/**
	 * @var string
	 */
	protected $related_model;

	/**
	 * @var Model
	 */
	protected $parent;

	/**
	 * @var bool
	 */
	protected $keep_synced = false;

	/**
	 * @var bool
	 */
	protected $cache;

	/**
	 * @var array
	 */
	private $registered_cache_events = array();

	/**
	 * @var string
	 */
	protected $attribute;

	/**
	 * @var Saver
	 */
	protected $saver;

	/**
	 * Relation constructor.
	 *
	 * @param string $related Class name of the related model.
	 * @param Model  $parent
	 * @param string $attribute
	 * @param Saver  $saver
	 */
	public function __construct( $related, Model $parent, $attribute, Saver $saver = null ) {

		if ( ! empty( $related ) && ! is_subclass_of( $related, 'IronBound\DB\Model' ) ) {
			throw new \InvalidArgumentException( '$related must be a subclass of IronBound\DB\Model' );
		}

		$this->related_model = $related;
		$this->parent        = $parent;
		$this->attribute     = $attribute;

		if ( $saver ) {
			$this->saver = $saver;
		} elseif ( $related ) {
			$this->saver = new ModelSaver( $related );
		} else {
			$this->saver = new ModelSaver();
		}

		$this->cache = (bool) $parent::get_event_dispatcher();
	}

	/**
	 * Keep the result collection synced.
	 *
	 * @since 2.0
	 *
	 * @param bool $keep_synced
	 *
	 * @return $this
	 */
	public function keep_synced( $keep_synced = true ) {
		$this->keep_synced = $keep_synced;

		return $this;
	}

	/**
	 * Cache the relation results.
	 *
	 * Defaults to true.
	 *
	 * @since 2.0
	 *
	 * @param bool $cache
	 *
	 * @return $this
	 */
	public function cache( $cache = true ) {
		$this->cache = $cache;

		return $this;
	}

	/**
	 * Get the results of the relation.
	 *
	 * @since 2.0
	 *
	 * @return Collection|Model|mixed
	 */
	public function get_results() {

		if ( ( $results = $this->load_from_cache( $this->parent ) ) === null ) {
			$results = $this->fetch_results();

			if ( $this->cache ) {
				$this->cache_results( $results, $this->parent );
				$this->maybe_register_cache_events();
			}
		}

		if ( $this->keep_synced && $results instanceof Collection ) {
			$this->register_events( $results );
		}

		return $results;
	}

	/**
	 * Load the results of a relation from the cache.
	 *
	 * @since 2.0
	 *
	 * @param Model $model
	 *
	 * @return Collection|Model|mixed|null
	 */
	protected function load_from_cache( Model $model ) {

		$cached = wp_cache_get( $model->get_pk(), $this->get_cache_group() );

		if ( $cached === false ) {
			return null;
		}

		if ( is_array( $cached ) ) {
			return $this->load_collection_from_cache( $cached, $model );
		} else {
			return $this->load_single_from_cache( $cached );
		}
	}

	/**
	 * Load a collection of models from the cache.
	 *
	 * @since 2.0
	 *
	 * @param int[]|string[] $cached
	 * @param Model          $for
	 *
	 * @return Collection
	 */
	protected function load_collection_from_cache( array $cached, Model $for ) {

		$models  = array();
		$removed = array();

		foreach ( $cached as $id ) {
			$model = $this->saver->get_model( $id );

			if ( $model ) {
				$models[ $id ] = $model;
			} else {
				$removed[] = $id;
			}
		}

		$diff = array_diff( $cached, $removed );
		wp_cache_set( $for->get_pk(), $diff, $this->get_cache_group() );

		return new Collection( $models, false, $this->saver );
	}

	/**
	 * Load a single model from the cache.
	 *
	 * @since 2.0
	 *
	 * @param int|string $cached
	 *
	 * @return object
	 */
	protected function load_single_from_cache( $cached ) {
		return $this->saver->get_model( $cached );
	}

	/**
	 * Cache the results of a relation.
	 *
	 * @since 2.0
	 *
	 * @param Collection|Model|mixed $results
	 * @param Model                  $model
	 */
	protected function cache_results( $results, Model $model ) {

		if ( $results instanceof Collection ) {
			$this->cache_collection( $results, $model );
		} elseif ( is_object( $results ) ) {
			$this->cache_single( $results, $model );
		}
	}

	/**
	 * Cache a collection of results.
	 *
	 * @since 2.0
	 *
	 * @param Collection $collection
	 * @param Model      $model
	 */
	protected function cache_collection( Collection $collection, Model $model ) {

		$ids = array_map( function ( $e ) use ( $collection ) {
			return $collection->get_saver()->get_pk( $e );
		}, $collection->toArray() );

		wp_cache_set( $model->get_pk(), $ids, $this->get_cache_group() );
	}

	/**
	 * Cache a single result.
	 *
	 * @since 2.0
	 *
	 * @param Model|mixed $result
	 * @param Model       $model
	 */
	protected function cache_single( $result, Model $model ) {

		$id = $this->saver->get_pk( $result );

		wp_cache_set( $model->get_pk(), $id, $this->get_cache_group() );
	}

	/**
	 * Maybe register cache events.
	 *
	 * Cache events should only be registered once per relation.
	 *
	 * @since 2.o
	 */
	private function maybe_register_cache_events() {

		$key = get_class( $this );
		$key .= "-{$this->attribute}";

		if ( isset( $this->registered_cache_events[ $key ] ) ) {
			return;
		}

		$this->register_cache_events();

		$this->registered_cache_events[ $key ] = true;
	}

	/**
	 * Register cache busting events.
	 *
	 * @since 2.0
	 */
	protected function register_cache_events() {

	}

	/**
	 * Get the cache group name.
	 *
	 * @since 2.0
	 *
	 * @return string
	 */
	final protected function get_cache_group() {
		return "relation:{$this->parent->table()->get_slug()}/{$this->attribute}";
	}

	/**
	 * Get the related model class.
	 *
	 * @since 2.0
	 *
	 * @return string
	 */
	public function get_related_model() {
		return $this->related_model;
	}

	/**
	 * Register model events for keeping the results in sync.
	 *
	 * @since 2.0
	 *
	 * @param Collection $results
	 */
	protected function register_events( Collection $results ) {

	}

	/**
	 * Fetch results from the database.
	 *
	 * @since 2.0
	 *
	 * @return mixed
	 */
	protected abstract function fetch_results();

	/**
	 * Eager-load a relation on a set of models.
	 *
	 * @since 2.0
	 *
	 * @param Model[]  $models   Array of models to eager-load. Keyed by their primary key.
	 * @param callable $callback Called with the FluentQuery object to customize the relations loaded.
	 *
	 * @return Collection All eager loaded models.
	 */
	public abstract function eager_load( array $models, $callback = null );

	/**
	 * Persist the results of a relation.
	 *
	 * @since 2.0
	 *
	 * @param Collection|Model|mixed $values
	 *
	 * @return Collection|Model|mixed Saved values.
	 */
	public abstract function persist( $values );

	/**
	 * Called when the parent model is deleted.
	 *
	 * Can be used to perform additional cleanup when a model is deleted,
	 * since the EventDispatcher is not guaranteed to be configured.
	 *
	 * @since 2.0
	 *
	 * @param Model $model
	 */
	public function on_delete( Model $model ) {
		wp_cache_delete( $model->get_pk(), $this->get_cache_group() );
	}
}


<?php
/**
 * Importable plugin for Craft CMS 3.x
 *
 * Add CSV data import functionality to the native Table fieldtype.
 *
 * @link      https://clickrain.com/about/mark-drzycimski
 * @copyright Copyright (c) 2017 Mark Drzycimski
 */

namespace markdrzy\importable;

use Craft;
use craft\base\Plugin;
use craft\services\Plugins;
use craft\events\PluginEvent;

use yii\base\Event;

/**
 * @author    Mark Drzycimski
 * @package   Importable
 * @since     1.0.0
 *
 */
class Importable extends Plugin
{
    // Static Properties
    // =========================================================================

    /**
     * Static property that is an instance of this plugin class so that it can be accessed via
     * Importable::$plugin
     *
     * @var Importable
     */
    public static $plugin;

    // Public Methods
    // =========================================================================

    public function init()
    {
        parent::init();
        self::$plugin = $this;

        // Load Asset Bundle
        if ( Craft::$app->request->getIsCpRequest() ) {
          Craft::$app->getView()->registerAssetBundle(ImportableAssets::class);
          Craft::$app->getView()->registerJs('Craft.importablePlugin.init({});');
        }

        Craft::info(
            Craft::t(
                'importable',
                '{name} plugin loaded',
                ['name' => $this->name]
            ),
            __METHOD__
        );
    }
}


<?php
/**
 * Contains the UserSaver class definition.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Saver;

/**
 * Class UserSaver
 *
 * @package IronBound\DB\Value
 */
class UserSaver extends Saver {

	/**
	 * @var string
	 */
	protected $key;

	/**
	 * UserSaver constructor.
	 *
	 * @param string $key
	 */
	public function __construct( $key = 'id' ) {

		if ( ! in_array( $key, array( 'id', 'login', 'slug' ) ) ) {
			throw new \InvalidArgumentException( "Invalid key '$key'." );
		}

		$this->key = $key;
	}

	/**
	 * @inheritDoc
	 */
	public function get_pk( $value ) {
		switch ( $this->key ) {
			case 'id':
				return $value->ID;
			case 'login':
				return $value->user_login;
			case 'slug':
				return $value->user_nicename;
			default:
				throw new \UnexpectedValueException( 'Unexpected key type.' );
		}
	}

	/**
	 * @inheritDoc
	 */
	public function get_model( $pk ) {
		switch ( $this->key ) {
			case 'id':
			case 'login':
			case 'slug':
				return get_user_by( $this->key, $pk ) ?: null;
			default:
				throw new \UnexpectedValueException( 'Unexpected key type.' );
		}
	}

	/**
	 * @inheritDoc
	 */
	public function make_model( $attributes ) {
		return new \WP_User( (object) $attributes );
	}

	/**
	 * @inheritDoc
	 */
	public function save( $value, array $options = array() ) {

		if ( ! $value instanceof \WP_User ) {
			throw new \InvalidArgumentException( 'ForeignUser can only save WP_User objects.' );
		}

		if ( ! $value->exists() ) {
			return $this->do_save( $value );
		}

		$current = get_user_by( 'id', $value->ID );

		if ( ! $current ) {
			return $this->do_save( $value );
		}

		$old = $current->to_array();
		$new = $value->to_array();

		if ( $this->has_changes( $old, $new ) ) {
			return $this->do_save( $value );
		}

		return $value;
	}

	/**
	 * Do the saving for a user.
	 *
	 * @since 2.0
	 *
	 * @param \WP_User $user
	 *
	 * @return \WP_User
	 */
	protected function do_save( \WP_User $user ) {

		if ( ! $user->exists() ) {

			if ( empty( $user->user_pass ) ) {
				$user->user_pass = wp_generate_password( 24 );
			}

			$id = wp_insert_user( wp_slash( $user->to_array() ) );
		} else {
			$id = wp_update_user( wp_slash( $user->to_array() ) );
		}

		if ( is_wp_error( $id ) ) {
			throw new \InvalidArgumentException( 'Error encountered while saving WP_User: ' . $id->get_error_message() );
		}

		return get_user_by( 'id', $id );
	}
}


<?php
/**
 * Contains the ModelWithMeta class definition.
 *
 * This is provided as an alternative to the meta trait for PHP 5.3.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Extensions\Meta;

use IronBound\DB\Model;

/**
 * Class ModelWithMeta
 * @package IronBound\DB\Model
 *
 * This class serves only to provide meta support for PHP 5.3 environments. In later PHP versions,
 * it is recommended to use the MetaSupport trait.
 */
abstract class ModelWithMeta extends Model implements WithMeta {

	/**
	 * @inheritDoc
	 */
	public function add_meta( $key, $value, $unique = false ) {

		add_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ), 10, 2 );
		$result = add_metadata( $this->get_meta_type(), $this->get_pk(), $key, wp_slash( $value ), $unique );
		remove_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ) );

		if ( $result !== false ) {
			$this->set_last_updated_at();
		}

		return $result;
	}

	/**
	 * @inheritDoc
	 */
	public function update_meta( $key, $value, $prev_value = '' ) {

		add_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ), 10, 2 );
		$result = update_metadata( $this->get_meta_type(), $this->get_pk(), $key, wp_slash( $value ), $prev_value );
		remove_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ) );

		if ( $result !== false ) {
			$this->set_last_updated_at();
		}

		return $result;
	}

	/**
	 * @inheritDoc
	 */
	public function get_meta( $key = '', $single = false ) {

		add_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ), 10, 2 );
		$result = get_metadata( $this->get_meta_type(), $this->get_pk(), $key, $single );
		remove_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ) );

		return $result;
	}

	/**
	 * @inheritDoc
	 */
	public function delete_meta( $key, $value = '', $delete_all = false ) {

		add_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ), 10, 2 );
		$result = delete_metadata( $this->get_meta_type(), $this->get_pk(), $key, wp_slash( $value ), $delete_all );
		remove_filter( 'sanitize_key', array( $this, '_meta_type_primary_id_override' ) );

		if ( $result !== false ) {
			$this->set_last_updated_at();
		}

		return $result;
	}

	/**
	 * Override the primary ID column by filtering the results of `sanitize_key()`.
	 *
	 * @since 2.0
	 *
	 * @internal
	 *
	 * @param string $key
	 * @param string $original
	 *
	 * @return string
	 */
	public function _meta_type_primary_id_override( $key, $original ) {

		if ( $original === $this->get_meta_type() . '_id' ) {
			$key = static::get_meta_table()->get_primary_id_column();
		}

		return $key;
	}

	/**
	 * @inheritdoc
	 */
	public static function get_meta_type() {

		/** @var \wpdb $wpdb */
		global $wpdb;

		$name = static::get_meta_table()->get_table_name( $wpdb );
		$name = preg_replace( "/^{$wpdb->prefix}/", "", $name );
		$name = preg_replace( "/meta$/", "", $name );

		return $name;
	}

	/**
	 * Set the model's last updated date whenever meta changes.
	 */
	private function set_last_updated_at() {

		if ( ! static::get_table() instanceof TimestampedTable ) {
			return;
		}

		$dirty = $this->is_dirty();

		$this->set_attribute( static::get_table()->get_updated_at_column(), $this->fresh_timestamp() );

		// If the model is dirty, we don't want to commit our save since the user should already be calling save.
		if ( ! $dirty ) {
			$this->save();
		}
	}
}