<?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 Model
 * @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 Model
 *
 * @category Model
 * @package  PicORM
 * @author   iNem0o <contact@inem0o.fr>
 * @license  LGPL http://opensource.org/licenses/lgpl-license.php
 * @link     https://github.com/iNem0o/PicORM
 */
abstract class Model
{
    /**
     * Table primary key
     *
     * @var null
     */
    protected static $_primaryKey = null;

    /**
     * Model Database name
     *
     * @var string
     */
    protected static $_databaseName = null;

    /**
     * Model table name
     *
     * @var string
     */
    protected static $_tableName = null;

    /**
     * Model relations
     *
     * @var array
     */
    protected static $_relations = array();

    /**
     * SQL fields from table without primary key
     *
     * @var array
     */
    protected static $_tableFields = null;

    /**
     * Array to store OOP declaration status of each Model subclass
     *
     * @var array
     */
    private static $_validationStatus = array();

    /**
     * Datasource instance
     *
     * @var \PDO
     */
    protected static $_dataSource;

    /**
     * Define if model have been saved
     *
     * @var bool
     */
    protected $_isNew = true;

    /**
     * Identifier for 1-1 relation
     */
    const ONE_TO_ONE = 1;

    /**
     * Identifier for 1-N relation
     */
    const ONE_TO_MANY = 2;

    /**
     * Identifier for N-N relation
     */
    const MANY_TO_MANY = 3;

    /**
     * Can declare model relations with calling
     * Define relations
     * ::addRelationOneToOne()
     * ::addRelationOneToMany()
     *
     * @throws Exception
     *
     * @return void
     */
    protected static function defineRelations()
    {

    }

    /**
     * Validate if this model is correctly implemented
     *
     * @return void
     * @throws Exception
     */
    protected static function _validateModel()
    {
        // check if model is already validate
        if (!isset(self::$_validationStatus[static::$_databaseName . static::$_tableName])) {

            // grab actual class name from late state binding
            $subClassName = get_class(new static());

            // check model OOP static structure is OK
            if (static::$_tableName === null) {
                throw new Exception($subClassName . '::$_tableName must be implemented');
            }
            if (static::$_primaryKey === null) {
                throw new Exception($subClassName . '::$_primaryKey must be implemented');
            }
            if (static::$_tableFields === null) {
                throw new Exception($subClassName . '::$_tableFields must be implemented');
            }
            if(in_array(static::$_primaryKey,static::$_tableFields)) {
                throw new Exception($subClassName . '::$_tableFields must not contain $_primaryKey');
            }
            // if user has implemented $_relations for this model, call the defineRelations() method
            if (static::$_relations !== null) {
                static::defineRelations();
            }

            // store model validation status
            self::$_validationStatus[static::$_databaseName . static::$_tableName] = true;
        }
    }

    /**
     * Format database name to using it in SQL query
     *
     * @return string
     */
    public static function formatDatabaseNameMySQL()
    {
        return !empty(static::$_databaseName) ? "`" . static::$_databaseName . "`." : '';
    }

    /**
     * Modify the database name on the fly
     * @param $databaseName
     */
    public static function setDatabaseName($databaseName) {
        static::$_databaseName = $databaseName;
    }

    /**
     * Format table name to using it in SQL query
     *
     * @return string
     */
    public static function formatTableNameMySQL()
    {
        return self::formatDatabaseNameMySQL() . "`" . static::$_tableName . "`";
    }

    /**
     * Modify the table name on the fly
     * @param $tableName
     */
    public static function setTableName($tableName) {
        static::$_tableName = $tableName;
    }

    /**
     * Return primary key field name
     *
     * @return string
     */
    public static function getPrimaryKeyFieldName()
    {
        return static::$_primaryKey;
    }

    /**
     * Save model in database
     *
     * @return bool
     */
    public function save()
    {
        if ($this->_isNew) {
            return $this->insert();
        } else {
            return $this->update();
        }
    }

    /**
     * Return model data in JSON
     *
     * @return string - json representation of the model
     */
    public function toJson()
    {
        return json_encode($this->toArray());
    }

    /**
     * Return model data in an array
     *
     * @param bool $includePrimary - Include or not primary key in returned array
     *
     * @return array
     */
    public function toArray($includePrimary = true)
    {
        if ($includePrimary) {
            $array = array(static::$_primaryKey => $this->{static::$_primaryKey});
        } else {
            $array = array();
        }

        foreach (static::$_tableFields as $unChamp) {
            $array[$unChamp] = $this->{$unChamp};
        }

        return $array;
    }

    /**
     * Return table field from Model
     *
     * @param bool $includePrimary - Include or not primary key in returned array
     *
     * @return array
     */
    public static function getModelFields($includePrimary = true)
    {
        $fields = array();

        if ($includePrimary) {
            $fields[] = static::$_primaryKey;
        }
        foreach (static::$_tableFields as $unChamp) {
            $fields[] = $unChamp;
        }

        return $fields;
    }

    /**
     * Magic call which create accessors for relation
     *
     * @param string $method - method name
     * @param array  $args   - method arguments
     *
     * @throws Exception
     *
     * @return mixed
     */
    public function __call($method, $args)
    {
        // validate model PHP structure if necessary
        static::_validateModel();

        // if method name begin with 'get' or 'set' or 'unset'
        // and finished with a knew relation name
        if (preg_match('/^(get|set|unset)(.+)/', $method, $matches)
            && array_key_exists(
                strtolower($matches[2]),
                static::$_relations
            )
        ) {

            // build relation accessor method name
            $toCall = '_' . $matches[1] . 'Relation';

            // calling getRelation() or setRelation() or unsetRelation() with relation data
            return $this->$toCall(static::$_relations[strtolower($matches[2])], $args);
        } else {
            // not a valid function
            throw new Exception("unknown method {$method}");
        }
    }

    /**
     * Unset a relation value from magic setter
     *
     * @param array $configRelation - Relation configuration
     * @param array $callArgs       - Models instance
     *
     * @todo unset other relations type
     * @return bool
     */
    private function _unsetRelation(array $configRelation, $callArgs)
    {
        $isDeleted = false;
        switch ($configRelation['typeRelation']) {
            case self::MANY_TO_MANY:
                // assure that we provide an array of relation to unset
                if (!is_array($callArgs[0])) {
                    $callArgs[0] = array($callArgs[0]);
                }
                foreach ($callArgs[0] as $oneRelationModel) {
                    // build delete query for this relationship
                    $query = new InternalQueryHelper();
                    $query->delete($configRelation['relationTable'])
                          ->where($configRelation['sourceField'], '=', '?')
                          ->where($configRelation['targetField'], '=', '?');

                    $query = static::$_dataSource->prepare($query->buildQuery());

                    $query->execute(
                          array(
                              $this->{$configRelation['sourceField']},
                              $oneRelationModel->{$configRelation['targetField']}
                          )
                    );
                }
                $isDeleted = true;
                break;
        }

        return $isDeleted;
    }

    /**
     * Set a relation value from magic setter
     *
     * @param array $configRelation - Relation configuration
     * @param array $callArgs       - Models instance
     *
     * @throws Exception
     *
     * @return bool
     */
    private function _setRelation(array $configRelation, $callArgs)
    {
        $isSaved = false;
        switch ($configRelation['typeRelation']) {
            case self::ONE_TO_ONE:
                if ($callArgs[0] instanceof $configRelation['classRelation']) {
                    // if relation model is new, we need to store it before
                    if ($callArgs[0]->isNew()) {
                        $callArgs[0]->save();
                    }

                    // set model field corresponding to the relation
                    $this->{$configRelation['sourceField']} = $callArgs[0]->{$configRelation['targetField']};

                    // if we have autoget field, grab and store them inside model
                    foreach ($configRelation['autoGetFields'] as $oneField) {
                        $this->{$oneField} = $callArgs[0]->{$oneField};
                    }
                    $isSaved = true;
                }
                break;
            case self::ONE_TO_MANY:
                if (is_array($callArgs[0])) {
                    // foreach models in relation
                    foreach ($callArgs[0] as $oneRelationModel) {
                        // set model field corresponding to the relation
                        $oneRelationModel->{$configRelation['targetField']} = $this->{$configRelation['sourceField']};
                        $oneRelationModel->save();
                    }
                    $isSaved = true;
                }
                break;
            case self::MANY_TO_MANY:
                // assure that we have an array of model
                if (!is_array($callArgs[0])) {
                    $callArgs[0] = array($callArgs[0]);
                }

                // prepare a query for testing if relation already exists
                $testQueryHelper = new InternalQueryHelper();
                $testQueryHelper->select('count(*) as nb')->from("`" . $configRelation['relationTable'] . "`")
                                ->where("`" . $configRelation['sourceField'] . "`", '=', '?')
                                ->where("`" . $configRelation['targetField'] . "`", '=', '?');
                $testQuery = static::$_dataSource->prepare($testQueryHelper->buildQuery());

                // prepare a query to insert relation between models
                $insertQuery = new InternalQueryHelper();
                $insertQuery->insertInto("`" . $configRelation['relationTable'] . "`")
                            ->values($configRelation['sourceField'], "?")
                            ->values($configRelation['targetField'], "?");
                $insertQuery = static::$_dataSource->prepare($insertQuery->buildQuery());

                // for each model in relation
                foreach ($callArgs[0] as $oneRelationModel) {
                    // if model is new, we need to store it before setting relation
                    if ($oneRelationModel->isNew()) {
                        $oneRelationModel->save();
                    } else {
                        // test if relation already exists between the two models
                        $testQuery->execute(
                                  array(
                                      $this->{$configRelation['sourceField']},
                                      $oneRelationModel->{$configRelation['targetField']}
                                  )
                        );
                        $errorcode = $testQuery->errorInfo();
                        if ($errorcode[0] != "00000") {
                            throw new Exception($errorcode[2]);
                        }
                        $testResult = $testQuery->fetch(\PDO::FETCH_ASSOC);

                        // relation already set
                        if ($testResult['nb'] > 0) {
                            continue;
                        }
                    }

                    // create link in relation table
                    $insertQuery->execute(
                                array(
                                    $this->{$configRelation['sourceField']},
                                    $oneRelationModel->{$configRelation['targetField']}
                                )
                    );
                    $errorcode = $insertQuery->errorInfo();

                    // check if data is stored with no error
                    if ($errorcode[0] != "00000") {
                        throw new Exception($errorcode[2]);
                    }

                    $isSaved = true;
                }
                break;
        }

        return $isSaved;
    }

    /**
     * Get a relation value using magic getter
     *
     * @param array $configRelation - Relation configuration
     * @param array $callArgs       - Models instance
     *
     * @throw Exception
     *
     * @return null
     */
    private function _getRelation(array $configRelation, $callArgs)
    {
        $where      = $order = array();
        $limitStart = $limitEnd = null;

        // extract find criteria from args
        if (isset($callArgs[0]) && is_array($callArgs[0])) {
            $where = $callArgs[0];
        }

        // extract order from args
        if (isset($callArgs[1]) && is_array($callArgs[1])) {
            $order = $callArgs[1];
        }

        // extract limit from args
        if (isset($callArgs[2]) && is_numeric($callArgs[2])) {
            $limitStart = $callArgs[2];
        }
        if (isset($callArgs[3]) && is_numeric($callArgs[3])) {
            $limitEnd = $callArgs[3];
        }

        $relationValue = null;
        switch ($configRelation['typeRelation']) {
            case self::ONE_TO_ONE:
                $classRelation = $configRelation['classRelation'];
                if (is_subclass_of($classRelation, 'PicORM\Model')) {
                    // add model relation relation field to where
                    $where = array_merge(
                        $where,
                        array($configRelation['targetField'] => $this->{$configRelation['sourceField']})
                    );

                    // grab the related model
                    $relationValue = $classRelation::findOne($where, $order);
                } else {
                    throw new Exception(sprintf("%s must be a subclass of PicORM\Model", $classRelation));
                }
                break;
            case self::ONE_TO_MANY:
                $classRelation = $configRelation['classRelation'];
                if (is_subclass_of($classRelation, 'PicORM\Model')) {
                    // add model relation relation field to where
                    $where = array_merge(
                        $where,
                        array($configRelation['targetField'] => $this->{$configRelation['sourceField']})
                    );

                    // grab the related models
                    $relationValue = $classRelation::find($where, $order, $limitStart, $limitEnd);
                } else {
                    throw new Exception(sprintf("%s must be a subclass of PicORM\Model", $classRelation));
                }
                break;
            case self::MANY_TO_MANY:
                $classRelation = $configRelation['classRelation'];
                if (is_subclass_of($classRelation, 'PicORM\Model')) {
                    // create the select query for related models using relation table
                    $selectRelations = new InternalQueryHelper();
                    $selectRelations
                        ->select("t.*")
                        ->from($classRelation::formatTableNameMySQL(), 't')
                        ->innerJoin(
                        $configRelation['relationTable'],
                        $configRelation['relationTable'] . "." . $configRelation['targetField'] . " = t." . $configRelation['targetField']
                        );
                    if($order !== null) {
                        foreach ($order as $orderField => $orderVal) {
                            $selectRelations->orderBy($orderField, $orderVal);
                        }
                    }
                    $selectRelations -> limit($limitStart, $limitEnd);

                    $where = $selectRelations->prefixWhereWithTable($where,'t');
                    $selectRelations->buildWhereFromArray($where);
                    // check one to one relation with auto get fields
                    // and append needed fields to select
                    $nbRelation = 0;
                    foreach ($classRelation::$_relations as $uneRelation) {
                        if ($uneRelation['typeRelation'] == self::ONE_TO_ONE
                            && count($uneRelation['autoGetFields']) > 0
                        ) {
                            // add auto get fields to select
                            foreach ($uneRelation['autoGetFields'] as &$oneField) {
                                $oneField = 'rel' . $nbRelation . "." . $oneField;
                            }

                            // add fields to select
                            $selectRelations->select($uneRelation['autoGetFields']);

                            // add query join corresponding to the relation
                            $relationFieldName = 'rel' . $nbRelation . '.`' . $uneRelation['targetField'];
                            $relationField     = 't' . '.`' . $uneRelation['sourceField'] . '`';
                            $selectRelations
                                ->leftJoin(
                                $uneRelation['classRelation']::formatTableNameMySQL() . ' rel' . $nbRelation,
                                $relationFieldName . '` = ' . $relationField
                                );

                            // increment relation count used in prefix
                            $nbRelation++;
                        }
                    }

                    $relatedFieldName = "`" . $configRelation['relationTable'] . "`." . $configRelation['sourceField'];
                    $conditionValue   = $this->{$configRelation['sourceField']};
                    $selectRelations->buildWhereFromArray(array($relatedFieldName => $conditionValue));

                    // create collection with this model datasource instance
                    // hydrating $classRelation model and using $selectRelation query helper
                    $relationValue = new Collection(static::getDataSource(), $selectRelations, $classRelation);
                } else {
                    throw new Exception(sprintf("%s must be a subclass of PicORM\Model", $classRelation));
                }
                break;
        }

        return $relationValue;
    }

    /**
     * Format class name without namespace to store a relation name
     *
     * @param string $fullClassName - Class name to normalize (with namespace)
     *
     * @return string
     */
    protected static function formatClassnameToRelationName($fullClassName)
    {
        // test if namespace present and remove them
        if (strpos($fullClassName, '\\') !== false) {
            $fullClassName = explode('\\', $fullClassName);
            $fullClassName = array_pop($fullClassName);
        }

        return strtolower($fullClassName);
    }

    /**
     * Add a OneToOne relation
     *
     * @param string $sourceField   - model source field
     * @param string $classRelation - relation model classname
     * @param string $targetField   - related model target field
     * @param array  $autoGetFields - field to auto get from relation when loading model
     * @param string $aliasRelation - override relation auto naming with an alias (ex : for reflexive relation)
     *
     * @throws Exception
     *
     * @return void
     */
    protected static function addRelationOneToOne(
        $sourceField,
        $classRelation,
        $targetField,
        $autoGetFields = array(),
        $aliasRelation = ''
    )
    {
        if (!is_string($sourceField)) {
            throw new Exception('$sourceField have to be a string');
        }
        if (!is_string($classRelation)) {
            throw new Exception('$classRelation have to be a string');
        }
        if (!is_string($targetField)) {
            throw new Exception('$targetField have to be a string');
        }

        // test is related class is a PicORM model
        if (!class_exists($classRelation) || !new $classRelation() instanceof Model) {
            throw new Exception("Class " . $classRelation . " doesn't exists or is not subclass of \PicORM\Model");
        }

        // test if its needed to autoget all fields
        if ($autoGetFields === true) {
            $autoGetFields = $classRelation::getModelFields();
        }

        // store autogetfields as an array
        if (!is_array($autoGetFields)) {
            $autoGetFields = array($autoGetFields);
        }

        // create relation id with normalized classRelation name
        $idRelation = self :: formatClassnameToRelationName($classRelation);

        // override the relation's id if an alias is present
        if (!empty($aliasRelation)) {
            $idRelation = strtolower($aliasRelation);
        }

        // store new relation in model
        static::$_relations[$idRelation] = array(
            'typeRelation'  => self::ONE_TO_ONE,
            'classRelation' => $classRelation,
            'sourceField'   => $sourceField,
            'targetField'   => $targetField,
            'autoGetFields' => $autoGetFields
        );
    }

    /**
     * Add a OneToMany relation
     *
     * @param        $sourceField   - model source field
     * @param        $classRelation - relation model classname
     * @param        $targetField   - related model target field
     * @param string $aliasRelation - override relation auto naming className with an alias
     *
     * @throws Exception
     */
    protected static function addRelationOneToMany($sourceField, $classRelation, $targetField, $aliasRelation = '')
    {
        // test is related class is a PicORM model
        if (!class_exists($classRelation) || !new $classRelation() instanceof Model) {
            throw new Exception("Class " . $classRelation . " doesn't exists or is not subclass of PicORM\Model");
        }

        // create relation id with normalized classRelation name
        $idRelation = self :: formatClassnameToRelationName($classRelation);

        // override the relation's id if an alias is present
        if (!empty($aliasRelation)) {
            $idRelation = strtolower($aliasRelation);
        }

        // store new relation in model
        static::$_relations[$idRelation] = array(
            'typeRelation'  => self::ONE_TO_MANY,
            'classRelation' => $classRelation,
            'sourceField'   => $sourceField,
            'targetField'   => $targetField,
        );
    }

    /**
     * Add a ManyToMany relation
     *
     * @param        $sourceField   - model source field
     * @param        $classRelation - relation model name
     * @param        $targetField   - related model field
     * @param        $relationTable - mysql table containing the two models ID
     * @param string $aliasRelation - override relation auto naming className
     *
     * @throws Exception
     */
    protected static function addRelationManyToMany(
        $sourceField,
        $classRelation,
        $targetField,
        $relationTable,
        $aliasRelation = ''
    )
    {
        // test is related class is a PicORM model
        if (!class_exists($classRelation) || !new $classRelation() instanceof Model) {
            throw new Exception("Class " . $classRelation . " doesn't exists or is not subclass of PicORM\Model");
        }

        // create relation id with normalized classRelation name
        $idRelation = self :: formatClassnameToRelationName($classRelation);

        // override the relation's id if an alias is present
        if (!empty($aliasRelation)) {
            $idRelation = strtolower($aliasRelation);
        }

        // store new relation in model
        static::$_relations[$idRelation] = array(
            'typeRelation'  => self::MANY_TO_MANY,
            'classRelation' => $classRelation,
            'sourceField'   => $sourceField,
            'targetField'   => $targetField,
            'relationTable' => $relationTable,
        );
    }

    /**
     * Return model array fetched from database with custom mysql query
     *
     * @param $query
     * @param $params
     *
     * @return static[]
     * @todo must return Collection
     */
    public static function findQuery($query, $params)
    {
        $query = static::$_dataSource->prepare($query);
        $query->execute($params);
        $fetch = $query->fetchAll(\PDO::FETCH_ASSOC);

        $collection = array();
        foreach ($fetch as $unRes) {
            $object = new static();
            $object->hydrate($unRes);
            $collection[] = $object;
        }

        return $collection;
    }

    /**
     * Return model collection fetched from database with criteria
     *
     * @param array $where      - associative array ex:
     *                          simple criteria        array('idMarque' => 1)
     *                          custom operator        array('idMarque' => array('operator' => '<=','value' => ''))
     *                          raw SQL                array('idMarque' => array('IN (5,6,4)'))
     * @param array $order      - associative array ex:array('libMarque'=>'ASC')
     * @param int   $limitStart - int
     * @param int   $limitEnd   - int
     *
     * @return Collection
     */
    public static function find($where = array(), $order = array(), $limitStart = null, $limitEnd = null)
    {
        // validate model PHP structure if necessary
        self :: _validateModel();

        // build a query helper with parameters
        $queryHelper = static::buildSelectQuery(array("*"), $where, $order, $limitStart, $limitEnd);

        // create a collection instance for called model with model datasource and custom created queryhelper
        return new Collection(static::$_dataSource, $queryHelper, get_called_class());
    }

    /**
     * Find one model from criteria
     *
     * @param array $where - associative array ex:
     *                     simple criteria        array('idMarque' => 1)
     *                     custom operator        array('idMarque' => array('operator' => '<=','value' => ''))
     *                     raw SQL                array('idMarque' => array('IN (5,6,4)'))
     * @param array $order - associative array ex:array('libMarque'=>'ASC')
     *
     * @return static
     */
    public static function findOne($where = array(), $order = array())
    {
        // try to fetch a model from database
        if ($dataModel = self::select(array('*'), $where, $order, 1)) {

            // hydrate new model instance with fetched data
            $model = new static();
            $model->hydrate($dataModel);

            return $model;
        } else {
            return null;
        }
    }

    /**
     * Test if model is already save in database
     *
     * @return bool
     */
    public function isNew()
    {
        return $this->_isNew;
    }

    /**
     * Count number of model in database from criteria
     *
     * @param array $where - associative array ex:
     *                     simple criteria        array('idMarque' => 1)
     *                     custom operator        array('idMarque' => array('operator' => '<=','value' => ''))
     *                     raw SQL                array('idMarque' => array('IN (5,6,4)'))
     *
     * @return int | null
     */
    public static function count($where = array())
    {
        // fetch the count with $where parameters
        $rawSqlFetch = self::select(array("count(*) as nb"), $where);

        return isset($rawSqlFetch[0]) && isset($rawSqlFetch[0]['nb']) ? (int)$rawSqlFetch[0]['nb'] : null;
    }

    /**
     * Build an InternalQueryHelper to select models
     *
     * @param array $fields     - selected fields
     * @param array $where      - associative array ex:
     *                          simple criteria       array('idMarque' => 1)
     *                          custom operator       array('idMarque' => array('operator' => '<=','value' => '5'))
     *                          raw SQL without operator       array('idMarque' => array('IN (5,6,4)')
     * @param array $order      - associative array ex:array('libMarque'=>'ASC')
     * @param int   $limitStart - int
     * @param int   $limitEnd   - int
     *
     * @return InternalQueryHelper
     */
    protected static function buildSelectQuery(
        $fields = array('*'),
        $where = array(),
        $order = array(),
        $limitStart = null,
        $limitEnd = null
    )
    {
        // get the formatted model mysql table name with database name
        $modelTableName = static::formatTableNameMySQL();

        // be sure that "*" is prefixed with model table name
        foreach ($fields as &$oneField) {
            if ($oneField == "*") {
                $oneField = $modelTableName . ".*";
                break;
            }
        }

        // create and helper to build a sql query
        $helper = new InternalQueryHelper();

        // prefix columns with model table name
        $where  = $helper->prefixWhereWithTable($where, $modelTableName);
        $orders = $helper->prefixOrderWithTable($order, $modelTableName);

        // starting build select
        $helper->select($fields)
               ->from($modelTableName);

        // check one to one relation with auto get fields
        // and append necessary fields to select
        $nbRelation = 0;
        foreach (static::$_relations as $uneRelation) {
            if ($uneRelation['typeRelation'] == self::ONE_TO_ONE && count($uneRelation['autoGetFields']) > 0) {
                // prefix fields
                foreach ($uneRelation['autoGetFields'] as &$oneField) {
                    $oneField = 'rel' . $nbRelation . "." . $oneField;
                }
                // add fields to select
                $helper->select($uneRelation['autoGetFields']);

                // add query join corresponding to the relation
                $helper->leftJoin(
                       $uneRelation['classRelation']::formatTableNameMySQL() . ' rel' . $nbRelation,
                       'rel' . $nbRelation . '.`' . $uneRelation['targetField'] . '` = ' . $modelTableName . '.`' . $uneRelation['sourceField'] . '`'
                );

                // increment relation count used in prefix
                $nbRelation++;
            }
        }

        // build where clause
        $helper->buildWhereFromArray($where);

        // build order clause
        foreach ($orders as $orderField => $orderVal) {
            $helper->orderBy($orderField, $orderVal);
        }

        // build limit clause
        $helper->limit($limitStart, $limitEnd);

        return $helper;
    }

    /**
     * Build a select mysql query for this model from criteria in parameters
     * return a raw mysql fetch assoc
     * Using Raw SQL _setVal, assume that you properly filter user input
     *
     * @param array $fields       - selected fields
     * @param array $where        - associative array ex:
     *                            simple criteria       array('idMarque' => 1)
     *                            custom operator       array('idMarque' => array('operator' => '<=','value' => '5'))
     *                            raw SQL without operator       array('idMarque' => array('IN (5,6,4)')
     * @param array $order        - associative array ex:array('libMarque'=>'ASC')
     * @param int   $limitStart   - int
     * @param int   $limitEnd     - int
     * @param int   $pdoFetchMode - PDO Fetch Mode (default : \PDO::FETCH_ASSOC)
     *
     * @return array
     * @throws Exception
     */
    public static function select(
        $fields = array('*'),
        $where = array(),
        $order = array(),
        $limitStart = null,
        $limitEnd = null,
        $pdoFetchMode = null
    )
    {
        // validate model PHP structure if necessary
        static::_validateModel();

        // build and execute query
        $mysqlQuery = static::buildSelectQuery($fields, $where, $order, $limitStart, $limitEnd);
        $query      = static::$_dataSource->prepare($mysqlQuery->buildQuery());
        $query->execute($mysqlQuery->getWhereParamsValues());

        // check for mysql error
        $errorcode = $query->errorInfo();
        if ($errorcode[0] != "00000") {
            throw new Exception($errorcode[2]);
        }

        // if no fetch mode specified fallback to FETCH_ASSOC
        if ($pdoFetchMode === null) {
            $pdoFetchMode = \PDO::FETCH_ASSOC;
        }

        // if limit say its a findOne only fetch
        if ($limitStart == 1 && ($limitEnd === null || $limitEnd === 1)) {
            return $query->fetch($pdoFetchMode);
        }

        // fetch and return data from database
        return $query->fetchAll($pdoFetchMode);
    }

    /**
     * Hydrate model from a fetch assoc
     * including OneToOne relation auto get field
     *
     * @param $data
     * @param $strictLoad - if true, all fields from $data will be loaded
     */
    public function hydrate($data, $strictLoad = true)
    {
        // using reflection to check if property exist
        $reflection = new \ReflectionObject($this);
        foreach ($data as $k => $v) {
            // if strictLoad is disabled, all properties are allowed to be hydrated
            if (!$strictLoad) {
                $this->{$k} = $v;
                continue;
            }
            // check if property really exists in class
            if ($reflection->hasProperty($k)) {
                $this->{$k} = $v;
                continue;
            }
            // test relation auto get fields
            foreach (static::$_relations as $uneRelation) {
                // check if this field is in auto get from relation
                if ($uneRelation['typeRelation'] == self::ONE_TO_ONE && in_array($k, $uneRelation['autoGetFields'])) {
                    $this->{$k} = $v;
                    break;
                }
            }
        }

        // model is not new anymore
        $this->_isNew = false;
    }

    /**
     * Delete this model from database
     *
     * @return array
     * @throws Exception
     */
    public function delete()
    {
        // validate model PHP structure if necessary
        static::_validateModel();

        // build delete query helper for this model
        $query = new InternalQueryHelper();
        $query->delete(self::formatTableNameMySQL())
              ->where(static::$_primaryKey, "=", "?");

        // delete model from database
        $query = static::$_dataSource->prepare($query->buildQuery());
        $query->execute(array($this->{static::$_primaryKey}));

        // check for mysql error
        $errorcode = $query->errorInfo();
        if ($errorcode[0] != "00000") {
            throw new Exception($errorcode[2]);
        }

        // model is not stored anymore in database
        $this->_isNew = true;

        return true;
    }

    /**
     * Update model field in database
     *
     * @return bool
     * @throws Exception
     */
    private function update()
    {
        // validate model PHP structure if necessary
        static::_validateModel();

        // build update query on model table
        $helper = new InternalQueryHelper();
        $helper->update(self::formatTableNameMySQL());

        // setting model fields value
        $params = array();
        foreach (static::$_tableFields as $unChamp) {
            // array is for raw SQL value
            if (is_array($this->$unChamp) && isset($this->{$unChamp}[0])) {
                $helper->set($unChamp, $this->{$unChamp}[0]);
            } else {
                // Mysql prepared value
                $helper->set($unChamp, '?');
                $params[] = $this->{$unChamp};
            }
        }

        // restrict with model primary key
        $helper->where(self::formatTableNameMySQL() . ".`" . static::$_primaryKey . "`", "=", "?");
        $params[] = $this->{static::$_primaryKey};

        // update model in database
        $query = static::$_dataSource->prepare($helper->buildQuery());
        $query->execute($params);

        // check for mysql error
        $errorcode = $query->errorInfo();
        if ($errorcode[0] != "00000") {
            throw new Exception($errorcode[2]);
        }

        return true;
    }

    /**
     * Insert model in database
     */
    private function insert()
    {
        // validate model PHP structure if necessary
        static::_validateModel();

        // create insert query for this model
        $queryHelp = new InternalQueryHelper();
        $queryHelp->insertInto(self::formatTableNameMySQL());

        // if primary key has forced value and is not present in tableField array
        if (!empty($this->{static::$_primaryKey}) && !in_array(static::$_primaryKey, static::$_tableFields)) {
            array_unshift(static::$_tableFields, static::$_primaryKey);
        } else {
            // use autoincrement for primary key
            $queryHelp->values(static::$_primaryKey, 'NULL');
        }

        // save model fields
        $params = array();
        foreach (static::$_tableFields as $unChamp) {
            // array is for raw SQL value
            if (is_array($this->$unChamp) && isset($this->{$unChamp}[0])) {
                $val = $this->{$unChamp}[0];
            } else {
                // mysql prepared value
                $val      = '?';
                $params[] = $this->$unChamp;
            }

            $queryHelp->values($unChamp, $val);
        }

        // execute insert query
        $query = static::$_dataSource->prepare($queryHelp->buildQuery());
        $query->execute($params);

        // check for mysql error
        $errorcode = $query->errorInfo();
        if ($errorcode[0] != "00000") {
            throw new Exception($errorcode[2]);
        }

        // model is saved, not new anymore
        $this->_isNew = false;

        // if empty PK grab the last insert ID for auto_increment fields
        if (empty($this->{static::$_primaryKey})) {
            $this->{static::$_primaryKey} = static::$_dataSource->lastInsertId();
        }

        return true;
    }

    /**
     * Initiates a transaction
     *
     * @return bool
     * @throws Exception
     */
    public static function begin()
    {
        // trying to start a mysql transation
        if (!$result = static::$_dataSource->beginTransaction()) {
            throw new Exception("Transaction could not begin!");
        }

        return $result;
    }

    /**
     * Rolls back a transaction
     *
     * @return boolean
     */
    public static function rollback()
    {
        return static::$_dataSource->rollBack();
    }

    /**
     * Commits a transaction
     *
     * @return boolean
     */
    public static function commit()
    {
        return static::$_dataSource->commit();
    }

    /**
     * Return PDO instance
     *
     * @return \PDO
     */
    public static function getDataSource()
    {
        return static::$_dataSource;
    }

    /**
     * Set PDO instance
     *
     * @param $_dataSource
     */
    public static function setDataSource($_dataSource)
    {
        static::$_dataSource = $_dataSource;
    }
}


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

namespace IronBound\DB;

use Closure;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection as DoctrineCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
use Doctrine\Common\Collections\Selectable;
use IronBound\DB\Saver\ModelSaver;
use IronBound\DB\Saver\Saver;

/**
 * Class Collection
 * @package IronBound\DB\Collections
 */
class Collection implements DoctrineCollection, Selectable {

	/**
	 * @var array
	 */
	protected $elements = array();

	/**
	 * Map of primary keys to indexes in the $elements array.
	 *
	 * This exists to provide O(1) lookup for models by their PK.
	 *
	 * @var array
	 */
	protected $pk_map = array();

	/**
	 * @var DoctrineCollection
	 */
	protected $added;

	/**
	 * @var DoctrineCollection
	 */
	protected $removed;

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

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

	/**
	 * ModelCollection constructor.
	 *
	 * @param DoctrineCollection|array $collection
	 * @param bool                     $keep_memory
	 * @param Saver                    $saver
	 */
	public function __construct( $collection = array(), $keep_memory = false, Saver $saver = null ) {

		if ( $collection instanceof DoctrineCollection ) {
			$collection = $collection->toArray();
		}

		$this->elements    = $collection;
		$this->keep_memory = $keep_memory;
		$this->saver       = $saver ?: new ModelSaver();

		$this->added   = new ArrayCollection();
		$this->removed = new ArrayCollection();

		foreach ( $this->elements as $i => $element ) {
			if ( $pk = $this->saver->get_pk( $element ) ) {
				$this->pk_map[ $pk ] = $i;
			}
		}
	}

	/**
	 * Clear the memory of added or removed elements.
	 *
	 * @since 2.0
	 */
	public function clear_memory() {
		$this->added->clear();
		$this->removed->clear();
	}

	/**
	 * Configure the collection to keep memory of added or removed elements.
	 *
	 * @since 2.0
	 *
	 * @param bool $keep_memory
	 *
	 * @return bool
	 */
	public function keep_memory( $keep_memory = true ) {

		$prev = $this->keep_memory;

		$this->keep_memory = $keep_memory;

		return $prev;
	}

	/**
	 * Get the models that have been added to this collection.
	 *
	 * This does not include models that the collection were instantiated with.
	 *
	 * @since 2.0
	 *
	 * @return DoctrineCollection
	 */
	public function get_added() {
		return $this->added;
	}

	/**
	 * Get the models that have been removed from this collection.
	 *
	 * @since 2.0
	 *
	 * @return DoctrineCollection
	 */
	public function get_removed() {
		return $this->removed;
	}

	/**
	 * Perform operations on the collection without remembering them.
	 *
	 * @since 2.0
	 *
	 * @param Closure $callback This ModelCollection is passed as the first parameter.
	 */
	public function dont_remember( Closure $callback ) {

		$prev = $this->keep_memory( false );

		$callback( $this );

		$this->keep_memory( $prev );
	}

	/**
	 * Retrieve a model by its primary key.
	 *
	 * @since 2.0
	 *
	 * @param string|int $pk
	 *
	 * @return Model|mixed|null
	 */
	public function get_model( $pk ) {

		$i = $this->get_model_index( $pk );

		if ( $i === - 1 ) {
			return null;
		}

		return $this->elements[ $i ];
	}

	/**
	 * Remove a model from the collection.
	 *
	 * @since 2.0
	 *
	 * @param string|int $pk
	 *
	 * @return bool
	 */
	public function remove_model( $pk ) {

		$i = $this->get_model_index( $pk );

		if ( $i === - 1 ) {
			return false;
		}

		$this->remove( $i );

		return true;
	}

	/**
	 * Get the index of a model.
	 *
	 * @since 2.0
	 *
	 * @param string|int $pk
	 *
	 * @return int
	 */
	protected function get_model_index( $pk ) {

		if ( isset( $this->pk_map[ $pk ] ) ) {
			return $this->pk_map[ $pk ];
		}

		foreach ( $this->elements as $i => $element ) {

			if ( $this->saver->get_pk( $element ) === $pk ) {

				$this->pk_map[ $pk ] = $i;

				return $i;
			}
		}

		return - 1;
	}

	/**
	 * Save all items in the collection.
	 *
	 * @since 2.0
	 *
	 * @param array $options
	 */
	public function save( array $options = array() ) {

		foreach ( $this->elements as $element ) {
			$this->saver->save( $element, $options );
		}
	}

	/**
	 * Get the saver backing this collection.
	 *
	 * @since 2.0
	 *
	 * @return Saver
	 */
	public function get_saver() {
		return $this->saver;
	}

	/**
	 * @inheritDoc
	 */
	public function add( $element ) {

		$this->elements[] = $element;

		if ( $this->keep_memory ) {
			$this->added[] = $element;
		}

		if ( $pk = $this->saver->get_pk( $element ) ) {
			$keys = $this->getKeys();

			$this->pk_map[ $pk ] = end( $keys );
		}

		return true;
	}

	/**
	 * @param array $items
	 */
	public function add_many( array $items ) {

		foreach ( $items as $element ) {

			$this->add( $element );
		}
	}

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

		$this->removed  = $this->elements;
		$this->elements = array();
		$this->pk_map   = array();
	}

	/**
	 * @inheritDoc
	 */
	public function contains( $element ) {

		if ( $pk = $this->saver->get_pk( $element ) ) {
			return (bool) $this->get_model( $pk );
		} else {
			return in_array( $element, $this->elements, true );
		}
	}

	/**
	 * @inheritDoc
	 */
	public function isEmpty() {
		return $this->count() === 0;
	}

	/**
	 * @inheritDoc
	 */
	public function remove( $key ) {

		$model = $this->get( $key );

		if ( $model && $pk = $this->saver->get_pk( $model ) ) {
			unset( $this->pk_map[ $pk ] );
		}

		if ( $this->keep_memory ) {
			$this->removed[] = $model;
		}

		unset( $this->elements[ $key ] );
	}

	/**
	 * @inheritDoc
	 */
	public function removeElement( $element ) {

		if ( $pk = $this->saver->get_pk( $element ) ) {

			if ( $this->keep_memory ) {
				$this->removed[] = $element;
			}

			if ( isset( $this->pk_map[ $pk ] ) ) {
				$i = $this->pk_map[ $pk ];

				unset( $this->elements[ $i ] );
				unset( $this->pk_map[ $pk ] );
			} else {

				foreach ( $this->elements as $i => $element ) {

					if ( $this->saver->get_pk( $element ) === $pk ) {
						unset( $this->elements[ $i ] );

						return;
					}
				}
			}
		} else {
			$i = array_search( $element, $this->elements, true );

			if ( $i !== false ) {
				unset( $this->elements[ $i ] );
			}
		}
	}

	/**
	 * @inheritDoc
	 */
	public function containsKey( $key ) {
		return isset( $this->elements[ $key ] ) ? true : false;
	}

	/**
	 * @inheritDoc
	 */
	public function get( $key ) {
		return isset( $this->elements[ $key ] ) ? $this->elements[ $key ] : null;
	}

	/**
	 * @inheritDoc
	 */
	public function getKeys() {
		return array_keys( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function getValues() {
		return array_values( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function set( $key, $value ) {

		$this->elements[ $key ] = $value;

		if ( $this->keep_memory ) {
			$this->added[] = $value;
		}

		if ( $pk = $this->saver->get_pk( $value ) ) {
			$this->pk_map[ $pk ] = $key;
		} else {
			$i = array_search( $key, $this->pk_map, true );

			if ( $i !== false ) {
				unset( $this->pk_map[ $i ] );
			}
		}
	}

	/**
	 * @inheritDoc
	 */
	public function toArray() {
		return $this->elements;
	}

	/**
	 * @inheritDoc
	 */
	public function first() {
		return reset( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function last() {
		return end( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function key() {
		return key( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function current() {
		return current( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function next() {
		return next( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function exists( Closure $p ) {

		foreach ( $this->elements as $key => $element ) {
			if ( $p( $key, $element ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @inheritDoc
	 *
	 * @return static
	 */
	public function filter( Closure $p ) {
		return new static( array_filter( $this->elements, $p ), $this->keep_memory, $this->saver );
	}

	/**
	 * @inheritDoc
	 */
	public function forAll( Closure $p ) {
		foreach ( $this->elements as $key => $element ) {
			if ( ! $p( $key, $element ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * @inheritDoc
	 *
	 * @return static
	 */
	public function map( Closure $func ) {
		return new static( array_map( $func, $this->elements ), $this->keep_memory, $this->saver );
	}

	/**
	 * @inheritDoc
	 *
	 * @return static[]
	 */
	public function partition( Closure $p ) {
		$matches = $noMatches = array();

		foreach ( $this->elements as $key => $element ) {
			if ( $p( $key, $element ) ) {
				$matches[ $key ] = $element;
			} else {
				$noMatches[ $key ] = $element;
			}
		}

		return array(
			new static( $matches, $this->keep_memory, $this->saver ),
			new static( $noMatches, $this->keep_memory, $this->saver )
		);
	}

	/**
	 * @inheritDoc
	 */
	public function indexOf( $element ) {

		if ( $pk = $this->saver->get_pk( $element ) ) {

			$i = $this->get_model_index( $pk );

			if ( $i !== - 1 ) {
				return $i;
			}
		}

		return array_search( $element, $this->elements, true );
	}

	/**
	 * @inheritDoc
	 */
	public function slice( $offset, $length = null ) {
		return array_slice( $this->elements, $offset, $length, true );
	}

	/**
	 * @inheritDoc
	 */
	public function getIterator() {
		return new \ArrayIterator( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function offsetExists( $offset ) {
		return $this->containsKey( $offset );
	}

	/**
	 * @inheritDoc
	 */
	public function offsetGet( $offset ) {
		return $this->get( $offset );
	}

	/**
	 * @inheritDoc
	 */
	public function offsetSet( $offset, $value ) {

		if ( ! isset( $offset ) ) {
			return $this->add( $value );
		}

		$this->set( $offset, $value );
	}

	/**
	 * @inheritDoc
	 */
	public function offsetUnset( $offset ) {
		return $this->remove( $offset );
	}

	/**
	 * @inheritDoc
	 */
	public function count() {
		return count( $this->elements );
	}

	/**
	 * @inheritDoc
	 */
	public function matching( Criteria $criteria ) {

		$expr     = $criteria->getWhereExpression();
		$filtered = $this->elements;

		if ( $expr ) {
			$visitor  = new ClosureExpressionVisitor();
			$filter   = $visitor->dispatch( $expr );
			$filtered = array_filter( $filtered, $filter );
		}

		if ( $orderings = $criteria->getOrderings() ) {
			foreach ( array_reverse( $orderings ) as $field => $ordering ) {
				$next = ClosureExpressionVisitor::sortByField( $field, $ordering == Criteria::DESC ? - 1 : 1 );
			}

			uasort( $filtered, $next );
		}

		$offset = $criteria->getFirstResult();
		$length = $criteria->getMaxResults();

		if ( $offset || $length ) {
			$filtered = array_slice( $filtered, (int) $offset, $length );
		}

		return new static( $filtered, $this->keep_memory, $this->saver );
	}
}


<?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 InternalQueryHelper
 * Extends raw QueryBuilder class to add some PicORM logic
 *
 * @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 InternalQueryHelper extends QueryBuilder
{
    /**
     * Values of where before sending to querybuilder
     *
     * @var array
     */
    protected $_whereValues;

    /**
     * Prefix fields from where associated array
     * with table name
     *
     * @param array  $data      - associative array with where data
     * @param string $tableName - table name to use as prefix
     *
     * @return mixed
     */
    public function prefixWhereWithTable($data, $tableName)
    {
        if (count($data) == 0) {
            return $data;
        }

        foreach ($data as $key => $v) {
            $newKey        = $tableName . '.' . trim($key, '`');
            $data[$newKey] = $v;
            unset($data[$key]);
        }

        return $data;
    }

    /**
     * Prefix fields from orderBy associated array
     * with table name
     *
     * @param array  $data      - associative array with order data
     * @param string $tableName - table name to use as prefix
     *
     * @return mixed
     */
    public function prefixOrderWithTable($data, $tableName)
    {
        if (count($data) == 0) {
            return $data;
        }

        foreach ($data as $key => $v) {
            // if $v is empty, we have a custom order like RAND() and do not have to prefix
            if (!empty($v)) {
                $newKey        = $tableName . '.' . trim($key, '`');
                $data[$newKey] = $v;
                unset($data[$key]);
            }
        }

        return $data;
    }

    /**
     * Build where condition from find() $where params
     *
     * @param array $where - associative array with where needed values
     *
     * @return $this
     */
    public function buildWhereFromArray(array $where)
    {
        if (count($where) == 0) {
            return $this->_whereValues;
        }

        foreach ($where as $fieldName => $oneCritera) {
            $val      = '?';
            $operator = "=";
            if (is_array($oneCritera)) {
                // using raw mysql
                if (count($oneCritera) == 1 && isset($oneCritera[0])) {
                    $val = $oneCritera[0];
                } else {
                    // custom operator
                    if (isset($oneCritera['operator'])) {
                        $operator = $oneCritera['operator'];
                    }
                    // custom value with prepared data or raw mysql if value is in an array
                    if (isset($oneCritera['value'])) {
                        if (is_array($oneCritera['value']) && isset($oneCritera['value'][0])) {
                            $val = $oneCritera['value'][0];
                        } else {
                            $this->_whereValues[] = $oneCritera['value'];
                        }
                    }
                }
            } else {
                $this->_whereValues[] = $oneCritera;
            }
            $this->where($fieldName, $operator, $val);
        }

        return $this->_whereValues;
    }

    /**
     * Return where values to prepare query
     *
     * @return mixed
     */
    public function getWhereParamsValues()
    {
        return $this->_whereValues;
    }

    /**
     * Clean query before switching to another type in collections
     *
     * @return $this
     */
    public function cleanQueryBeforeSwitching()
    {
        // clean join created by collection for fetching auto get field
        $this->_join = array();

        return $this;
    }
}


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

namespace IronBound\DB\Table\Column;

/**
 * Class BaseColumn
 * @package IronBound\DB\Column
 */
abstract class BaseColumn implements Column {

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

	/**
	 * @var array
	 */
	protected $options = array();

	/**
	 * @var array
	 */
	protected $type_options = array();

	/**
	 * BaseColumn constructor.
	 *
	 * @param string $name         Name of this column.
	 * @param array  $options      Additional options for this column. For example, 'NOT NULL'.
	 * @param array  $type_options Type options. For example '20' in 'BIGINT(20)'.
	 */
	public function __construct( $name, array $options = array(), array $type_options = array() ) {
		$this->name         = $name;
		$this->options      = $options;
		$this->type_options = $type_options;
	}

	/**
	 * @inheritdoc
	 */
	public function get_definition() {
		return "`{$this->name}` {$this->get_definition_without_column_name()}";
	}

	/**
	 * @inheritDoc
	 */
	public function __toString() {
		return $this->get_definition();
	}

	/**
	 * Get the column definition without the column name attached.
	 *
	 * @since 2.0
	 *
	 * @param array $exclude_options
	 *
	 * @return string
	 */
	protected function get_definition_without_column_name( array $exclude_options = array() ) {

		$definition = $this->get_mysql_type();

		if ( $this->type_options ) {
			$definition .= '(' . implode( ',', $this->type_options ) . ')';
		}

		if ( $this->options ) {

			$options = array_udiff( $this->options, $exclude_options, function ( $a, $b ) {
				return strcmp( strtolower( $a ), strtolower( $b ) );
			} );

			$definition .= ' ' . implode( ' ', $options );
		}

		return $definition;
	}

	/**
	 * @inheritdoc
	 */
	abstract public function get_mysql_type();

	/**
	 * @inheritdoc
	 */
	abstract public function convert_raw_to_value( $raw );

	/**
	 * @inheritdoc
	 */
	abstract public function prepare_for_storage( $value );
}


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

namespace IronBound\DB;

use IronBound\DB\Query\Complex_Query;
use IronBound\DB\Query\Simple_Query;
use IronBound\DB\Table\Plugins\Plugin;
use IronBound\DB\Table\Table;

/**
 * Class Manager
 *
 * @package IronBound\DB
 */
final class Manager {

	/**
	 * @var Table[]
	 */
	private static $tables = array();

	/**
	 * @var Plugin[]
	 */
	private static $plugins = array();

	/**
	 * Register a db table.
	 *
	 * @since 1.0
	 *
	 * @param Table  $table Table object.
	 * @param string $complex_query_class
	 * @param string $model_class
	 *
	 * @throws \InvalidArgumentException If $complex_query_class is not a subclass of Complex_Query.
	 */
	public static function register( Table $table, $complex_query_class = '', $model_class = '' ) {

		if ( $complex_query_class && ! is_subclass_of( $complex_query_class, 'IronBound\DB\Query\Complex_Query' ) ) {
			throw new \InvalidArgumentException( '$complex_query_class must subclass Complex_Query' );
		}

		if ( $model_class && ! is_subclass_of( $model_class, 'IronBound\DB\Model' ) ) {
			throw new \InvalidArgumentException( '$model_class must subclass Model.' );
		}

		static::$tables[ $table->get_slug() ] = array(
			'table' => $table,
			'query' => $complex_query_class,
			'model' => $model_class
		);

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

		$name = $table->get_table_name( $wpdb );
		$name = preg_replace( "/^{$wpdb->prefix}/", "", $name );

		$wpdb->{$name}  = $table->get_table_name( $wpdb );
		$wpdb->tables[] = $name;

		static::fire_plugin_event( $table, 'registered' );
	}

	/**
	 * Retrieve a db table object.
	 *
	 * @since 1.0
	 *
	 * @param string $slug
	 *
	 * @return Table|null
	 */
	public static function get( $slug ) {

		if ( isset( static::$tables[ $slug ] ) ) {
			return static::$tables[ $slug ]['table'];
		} else {
			return null;
		}
	}

	/**
	 * Get all tables.
	 *
	 * @since 2.0.0
	 *
	 * @return Table[]
	 */
	public static function all() {
		$tables = array();

		foreach ( static::$tables as $table ) {
			$tables[] = $table['table'];
		}

		return $tables;
	}

	/**
	 * Make a query object for the selected db table.
	 *
	 * @since 1.0
	 *
	 * @param string $slug Table name.
	 * @param \wpdb  $wpdb
	 *
	 * @return Simple_Query|null
	 */
	public static function make_simple_query_object( $slug, \wpdb $wpdb = null ) {

		$table = static::get( $slug );
		$wpdb  = $wpdb ?: $GLOBALS['wpdb'];

		if ( $table ) {
			return new Simple_Query( $wpdb, $table );
		} else {
			return null;
		}
	}

	/**
	 * Make a complex query object.
	 *
	 * @since 1.0
	 *
	 * @param string $slug
	 * @param array  $args
	 *
	 * @return Complex_Query|null
	 */
	public static function make_complex_query_object( $slug, array $args = array() ) {

		$table = static::get( $slug );

		if ( empty( $table ) || empty( static::$tables[ $slug ]['query'] ) ) {
			return null;
		}

		$class = static::$tables[ $slug ]['query'];

		$query = new $class( $args );

		return $query;
	}

	/**
	 * Get the model for a table.
	 *
	 * @since 2.0
	 *
	 * @param string|Table $slug
	 *
	 * @return string|null
	 */
	public static function get_model( $slug ) {

		$slug = $slug instanceof Table ? $slug->get_slug() : $slug;

		if ( isset( static::$tables[ $slug ] ) ) {
			return static::$tables[ $slug ]['model'];
		} else {
			return null;
		}
	}

	/**
	 * Maybe install a table.
	 *
	 * Will skip a table if it is up-to-date.
	 *
	 * @since 1.0
	 *
	 * @param Table $table
	 * @param \wpdb $wpdb
	 *
	 * @return bool True if installed or updated, false if skipped.
	 * @throws \IronBound\DB\Exception
	 */
	public static function maybe_install_table( Table $table, \wpdb $wpdb = null ) {

		$wpdb = $wpdb ?: $GLOBALS['wpdb'];

		$installed = (int) get_option( $table->get_table_name( $wpdb ) . '_version', 0 );

		if ( $installed >= $table->get_version() ) {
			return false;
		}

		if ( $installed === 0 ) {
			if ( ! static::is_table_installed( $table, $wpdb ) ) {
				$prev = $wpdb->show_errors( false );
				$wpdb->query( $table->get_creation_sql( $wpdb ) );
				$wpdb->show_errors( $prev );

				if ( $wpdb->last_error ) {
					throw new Exception( $wpdb->last_error );
				}

				static::fire_plugin_event( $table, 'installed' );
			}
		} else {

			$update = $installed + 1;

			while ( $update <= $table->get_version() ) {

				if ( method_exists( $table, "v{$update}_schema_update" ) ) {
					$method = "v{$update}_schema_update";

					$table->{$method}( $wpdb, $installed );

					static::fire_plugin_event( $table, 'updated_schema', array( $update, $installed ) );
				}

				$update += 1;
			}

			static::fire_plugin_event( $table, 'updated' );
		}

		update_option( $table->get_table_name( $wpdb ) . '_version', $table->get_version() );

		return true;
	}

	/**
	 * Maybe delete a table.
	 *
	 * @since 2.0
	 *
	 * @param Table $table
	 * @param \wpdb $wpdb
	 *
	 * @return bool Return true if table is installed and deleted. False otherwise.
	 */
	public static function maybe_uninstall_table( Table $table, \wpdb $wpdb = null ) {

		$wpdb = $wpdb ?: $GLOBALS['wpdb'];

		if ( static::is_table_installed( $table, $wpdb ) ) {

			$tn = $table->get_table_name( $wpdb );

			$wpdb->query( "DROP TABLE IF EXISTS `{$tn}`" );
			static::fire_plugin_event( $table, 'deleted' );

			delete_option( $tn . '_version' );

			return true;
		}

		return false;
	}

	/**
	 * Maybe empty a table.
	 *
	 * @since 2.0
	 *
	 * @param Table $table
	 * @param \wpdb $wpdb
	 *
	 * @return bool Return true if table is installed and emptied. False otherwise.
	 */
	public static function maybe_empty_table( Table $table, \wpdb $wpdb = null ) {

		$wpdb = $wpdb ?: $GLOBALS['wpdb'];

		if ( static::is_table_installed( $table, $wpdb ) ) {

			$tn = $table->get_table_name( $wpdb );

			$wpdb->query( "TRUNCATE TABLE `{$tn}`" );
			static::fire_plugin_event( $table, 'emptied' );

			return true;
		}

		return false;
	}

	/**
	 * Check if a table is installed.
	 *
	 * @since 1.0
	 *
	 * @param Table $table
	 * @param \wpdb $wpdb
	 *
	 * @return bool
	 */
	public static function is_table_installed( Table $table, \wpdb $wpdb = null ) {

		$wpdb = $wpdb ?: $GLOBALS['wpdb'];

		$name = $table->get_table_name( $wpdb );

		$results = $wpdb->get_results( "SHOW TABLES LIKE '$name'" );

		return is_array( $results ) && count( $results ) > 0;
	}

	/**
	 * Register a plugin with the table manager.
	 *
	 * @since 2.0
	 *
	 * @param Plugin $plugin
	 */
	public static function register_plugin( Plugin $plugin ) {
		static::$plugins[] = $plugin;
	}

	/**
	 * Fire a plugin event on a given table.
	 *
	 * @since 2.0
	 *
	 * @param Table  $table
	 * @param string $event Name of the method on the plugin object. Either 'registered', 'installed', or 'updated'.
	 * @param array  $args  Additional arguments to pass to the plugin handler.
	 */
	protected static function fire_plugin_event( Table $table, $event, $args = array() ) {

		foreach ( static::$plugins as $plugin ) {
			if ( $plugin->accepts( $table ) ) {
				if ( method_exists( $plugin, $event ) ) {
					$args = array( $table ) + $args;
					call_user_func_array( array( $plugin, $event ), $args );
				}
			}
		}
	}
}


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

namespace IronBound\DB\Saver;

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

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

	/**
	 * @inheritDoc
	 */
	public function get_model( $pk ) {
		return get_post( $pk );
	}

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

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

		if ( ! $value instanceof \WP_Post ) {
			throw new \InvalidArgumentException( sprintf(
				'ForeignPost can only save WP_Post objects, %s given.', is_object( $value ) ? get_class( $value ) : gettype( $value )
			) );
		}

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

		$current = get_post( $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 post.
	 *
	 * @since 2.0
	 *
	 * @param \WP_Post $post
	 *
	 * @return \WP_Post
	 */
	protected function do_save( \WP_Post $post ) {

		if ( ! $post->ID ) {
			$id = wp_insert_post( wp_slash( $post->to_array() ), true );
		} else {
			$id = wp_update_post( $post, true );
		}

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

		return get_post( $id );
	}
}


<?php
/**
 * Contains the class for the Foreign column type.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2016.
 */

namespace IronBound\DB\Table\Column;

use IronBound\DB\Table\Table;

/**
 * Class SimpleForeign
 * @package IronBound\DB\Table\Column
 */
class SimpleForeign extends BaseColumn implements Foreign {

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

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

	/**
	 * SimpleForeign constructor.
	 *
	 * @param string      $name           Column name.
	 * @param Table       $foreign_table  Table the foreign key resides in.
	 * @param string|null $foreign_column Specify the column being related to. If null, the primary key column is used.
	 */
	public function __construct( $name, Table $foreign_table, $foreign_column = null ) {
		parent::__construct( $name );

		$this->foreign_table  = $foreign_table;
		$this->foreign_column = $foreign_column;
	}

	/**
	 * @inheritDoc
	 */
	public function get_foreign_table() {
		return $this->foreign_table;
	}

	/**
	 * @inheritDoc
	 */
	public function get_foreign_table_column_name() {
		return $this->foreign_column ?: $this->foreign_table->get_primary_key();
	}

	/**
	 * Get the referenced column.
	 *
	 * @since 2.0
	 *
	 * @return BaseColumn
	 */
	protected function get_column() {

		$column  = $this->foreign_column ?: $this->foreign_table->get_primary_key();
		$columns = $this->foreign_table->get_columns();

		return $columns[ $column ];
	}

	/**
	 * @inheritDoc
	 */
	public function get_definition() {
		return "`{$this->name}` {$this->get_column()->get_definition_without_column_name( array( 'auto_increment' ) )}";
	}

	/**
	 * @inheritDoc
	 */
	public function get_mysql_type() {
		return $this->get_column()->get_mysql_type();
	}

	/**
	 * @inheritDoc
	 */
	public function convert_raw_to_value( $raw ) {
		return $this->get_column()->convert_raw_to_value( $raw );
	}

	/**
	 * @inheritDoc
	 */
	public function prepare_for_storage( $value ) {
		return $this->get_column()->prepare_for_storage( $value );
	}
}


<?php
/**
 * InMemoryTable class.
 *
 * @author    Iron Bound Designs
 * @since     2.0
 * @license   MIT
 * @copyright Iron Bound Designs, 2017.
 */

namespace IronBound\DB\Table;

use IronBound\DB\Extensions\Trash\TrashTable;
use IronBound\DB\Table\Column\Column;
use IronBound\DB\Table\Column\ForeignModel;

/**
 * Class InMemoryTable
 *
 * @package IronBound\DB\Table
 */
class InMemoryTable extends BaseTable implements TrashTable {

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

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

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

	/** @var Column[] */
	private $columns = array();

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

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

	/** @var array */
	protected $args;

	/**
	 * InMemoryTable constructor.
	 *
	 * @param string   $name
	 * @param Column[] $columns
	 * @param array    $args
	 */
	public function __construct( $name, array $columns, array $args = array() ) {
		$this->name            = $name;
		$this->columns_defined = $columns;

		if ( isset( $args['slug'] ) ) {
			$this->slug = $args['slug'];
		} else {
			$this->slug = str_replace( '_', '-', strtolower( $name ) );
		}

		if ( isset( $args['defaults'] ) ) {
			$this->defaults = $args['defaults'];
		}

		$this->args        = $args;
		$this->primary_key = empty( $args['primary-key'] ) ? 'id' : $args['primary-key'];
	}

	/**
	 * Parse defaults based on the column types.
	 *
	 * @param array $columns
	 */
	protected function parse_defaults( array $columns ) {
		foreach ( $columns as $column_name => $column ) {
			if ( array_key_exists( $column_name, $this->defaults ) ) {
				continue;
			}

			switch ( $column->get_mysql_type() ) {
				case 'TINYINT':
				case 'SMALLINT':
				case 'MEDIUMINT':
				case 'INT':
				case 'BIGINT':
					$this->defaults[ $column_name ] = 0;
					break;
				case 'FLOAT':
				case 'DOUBLE':
				case 'DECIMAL':
					$this->defaults[ $column_name ] = 0.0;
					break;
				case 'DATE':
				case 'DATETIME':
					$this->defaults[ $column_name ] = null;
					break;
				default:
					$this->defaults[ $column_name ] = '';
					break;
			}
		}
	}

	/**
	 * @inheritDoc
	 */
	public function get_table_name( \wpdb $wpdb ) { return $wpdb->prefix . $this->name; }

	/**
	 * @inheritDoc
	 */
	public function get_slug() { return $this->slug; }

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

		if ( $this->columns ) {
			return $this->columns;
		}

		foreach ( $this->columns_defined as $name => $column ) {
			if ( $column instanceof \Closure ) {
				$column = $column();
			}

			$this->columns[ $name ] = $column;
		}

		$this->parse_defaults( $this->columns );

		return $this->columns;
	}

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

		if ( ! $this->columns ) {
			$this->get_columns();
		}

		return $this->defaults;
	}

	/**
	 * @inheritDoc
	 */
	public function get_primary_key() { return $this->primary_key; }

	/**
	 * @inheritDoc
	 */
	public function get_version() { return 1; }

	/**
	 * @inheritDoc
	 */
	public function get_deleted_at_column() {
		$columns = $this->get_columns();

		return isset( $columns['deleted_at'] ) ? 'deleted_at' : null;
	}
}