<?php
/**
 * Active record class
 * Extends the ModelBase class
 * Implements the JsonSerializable interface
 * Implements the IModel interface
 * @author Jérôme Brilland
 * @version 2.3
 * @copyright (c) 2018-2023, Jérôme Brilland
 * @license http://www.gnu.org/licenses/gpl.txt GNU General Public License
 * @abstract
 */
abstract class ActiveRecord extends ModelBase implements JsonSerializable, IModel {

    /**
     * @var string Table name
     * @access protected
     * @static
     */
    protected static $tableName;

    /**
     * @var string Primary key
     * @access protected
     * @static
     */
    protected static $primaryKey;

    /**
     * @var array Attributes
     * @access protected
     */
    protected $attributes = [];

    /**
     * @var array Updates
     * @access protected
     */
    protected $updates = [];
    
    /**
     * @var array Validation errors
     * @access protected
     */
    protected $errors = [];

    private $newRecord = true;
    
    /**
     * @var boolean
     * @access public
     * @static
     */
    public static $recordTimestamps = true; 

    /**
     * Class constructor
     * @access public
     */
    public function __construct( $attributes = null, $newRecord = true ) {
        parent::__construct();
        if ( is_array( $attributes ) ) {
            foreach ( $attributes as $name => $value ) {
                $this->setAttribute( $name, $value );
            }
        }
        $this->newRecord = $newRecord;
        $this->initialize();
        $this->notify( 'after_initialize' );
    }

    protected function initialize() {

    }

    /**
     * Returns the database connection
     * @return Medoo
     * @access protected
     */
    protected static function getDatabase() {
        return IoC::resolve( 'database' );
    }

    /**
     * Return the table name
     * @return string The table name
     * @access public
     * @static
     */
    public static function getTableName() {
        if ( isset( static::$tableName ) ) {
            return static::$tableName;
        }
        $class = new ReflectionClass( get_called_class() );
        return strtolower( $class->getShortName() );
    }

    /**
     * Return the primary key
     * @return string The primary key
     * @access public
     * @static
     */
    public static function getPrimaryKey() {
        if ( isset( static::$primaryKey ) ) {
            return static::$primaryKey;
        }
        return self::getTableName() . '_id';
    }

    public static function getColumnNames() {
        $schema = IoC::resolve( 'sqlschema' );
        return $schema->getColumnNames( self::getTableName() );
    }

    /**
     * Returns the selected record.
     * @return ActiveRecord A single model object
     * @throws ErrorException
     * @access public
     * @static
     */
    public static function findOne( ...$arguments ) {
        $database = self::getDatabase();
        $logger = IoC::resolve( 'logger' );
        if ( (1 == count( $arguments ) ) && is_numeric( $arguments[0] ) ) {
            $primaryKey = self::getPrimaryKey();
            $data = $database->get( self::getTableName(), '*', [$primaryKey => $arguments[0]] );
        } elseif ( (1 == count( $arguments )) && is_array( $arguments[0] ) ) {
            $data = $database->get( self::getTableName(), '*', $arguments[0] );
        } elseif ( 2 == count( $arguments ) ) {
            $data = $database->get( self::getTableName(), $arguments[0], $arguments[1] );
        } elseif ( 3 == count( $arguments ) ) {
            $data = $database->get( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        } else {
            throw new LogicException( 'Too many arguments' );
        }
        if ( DEVELOPMENT_ENVIRONMENT ) {  
            $logger->write( sprintf( '[%s] [%s] [%s] %s', date( 'Y-m-d H:i:s' ), __FILE__, 'debug', $database->last() ) );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        if ( $data ) {
            $className = get_called_class();
            $record = new $className( $data, false );
            $record->notify( 'after_find' );
            $record->afterFind();
            IoC::resolve( 'eventmediator' )->trigger( 'after_find', [$record] );
            return $record;
        } else {
            return NULL;
        }
    }

    /**
     * Finds all the records matching the given criteria.
     * @param type $arguments
     * @return array
     * @throws LogicException
     * @throws ErrorException
     * @access public
     * @static
     */
    public static function findMany( ...$arguments ) {
        $database = self::getDatabase();
        $logger = IoC::resolve( 'logger' );
        if ( 1 == count( $arguments) ) {
            $data = $database->select( self::getTableName(), '*', $arguments[0] );
        } elseif ( 2 == count( $arguments) ) {
            $data = $database->select( self::getTableName(), $arguments[0], $arguments[1] );
        } elseif ( 3 == count( $arguments) ) {
            $data = $database->select( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        } else {
            throw new LogicException( 'Too many arguments' );
        }
        if ( DEVELOPMENT_ENVIRONMENT ) {  
            $logger->write( sprintf( '[%s] [%s] [%s] %s', date( 'Y-m-d H:i:s' ), __FILE__, 'debug', $database->last() ) );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        $records = [];
        foreach ( $data as $row ) {
            $className = get_called_class();
            $record = new $className( $row, false );
            $record->notify( 'after_find' );
            $record->afterFind();
            $records[] = $record;
        }
        IoC::resolve( 'eventmediator' )->trigger( 'after_find', [$records] );
        return $records;
    }

    /**
     * Finds all the records matching the given criteria.
     * @param type $arguments
     * @return array
     * @throws LogicException
     * @throws ErrorException
     * @access public
     * @static
     */
    public static function findAll( ...$arguments ) {
        $database = self::getDatabase();
        $logger = IoC::resolve( 'logger' );
        if ( 0 == count( $arguments ) ) { //no argument
            $data = $database->select( self::getTableName(), '*' );
        } elseif ( 1 == count( $arguments ) ) { //1 argument : columns
            $data = $database->select( self::getTableName(), $arguments[0] );
        } elseif ( 2 == count( $arguments )) { //2 arguments : joins, columns
            $data = $database->select( self::getTableName(), $arguments[0], $arguments[1] );
        } else {
            throw new LogicException( 'Too many arguments' );
        }
        if ( DEVELOPMENT_ENVIRONMENT ) {  
            $logger->write( sprintf( '[%s] [%s] [%s] %s', date( 'Y-m-d H:i:s' ), __FILE__, 'debug', $database->last() ) );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        $records = [];
        foreach ( $data as $row ) {
            $className = get_called_class();
            $record = new $className( $row, false );
            $record->notify( 'after_find' );
            $record->afterFind();
            $records[] = $record;
        }
        IoC::resolve( 'eventmediator' )->trigger( 'after_find', [$records] );
        return $records;
    }
    
    /**
     * Finds the first record matching the attributes or a new instance.
     * @param array $where
     * @param array $attributes
     * @return array
     * @access public
     * @static
     */
    public static function findOrNew( array $where, array $attributes = [] ) {
        $record = self::findOne( '*', $where );
        if ( isset( $record ) ) {
            foreach ( $attributes as $name => $value ) {
                $record->$name = $value;
            }
        } else {
            $className = get_called_class();
            $record = new $className( array_merge( $where, $attributes ) );
        }
        return $record;
    }
    
    /**
     * Updates an existing model matching the given criteria, or creates a new model if no matching model exists. 
     * @param array $where
     * @param array $attributes
     * @return array
     * @access public
     * @static
     */
    public static function updateOrCreate( array $where, array $attributes ) {
        $record = self::findOrNew( $where, $attributes );
        return $record->save();
    }

    /**
     * Returns true if a record exists in the table that matches the id or conditions given, or false otherwise
     * @return boolean
     * @throws ErrorException
     * @static
     */
    public static function exists( ...$arguments ) {
        $database = self::getDatabase();
        if ( ( 1 == count( $arguments ) ) && is_numeric( $arguments[0] ) ) {
            $primaryKey = self::getPrimaryKey();
            $result = $database->has( self::getTableName(), [$primaryKey => $arguments[0]] );
        } elseif ( 1 == count( $arguments) ) {
            $result = $database->has( self::getTableName(), $arguments[0] );
        } elseif ( 2 == count( $arguments ) ) {
            $result = $database->has( self::getTableName(), $arguments[0], $arguments[1] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        return $result;
    }

    /**
     * Returns the number of rows.
     * @return int The number of rows
     * @throws ErrorException
     * @access public
     */
    public static function count( ...$arguments ) {
        $database = self::getDatabase();
        if ( 0 == count( $arguments ) ) {
            $result = $database->count( self::getTableName() );
        } elseif ( 1 == count( $arguments ) ) {
            $result = $database->count( self::getTableName(), $arguments[0] );
        } elseif ( 3 == count( $arguments ) ) {
            $result = $database->count( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        return $result;
    }

    /**
     *
     * @param type $arguments
     * @return type
     * @throws ErrorException
     * @access public
     */
    public static function maximum( ...$arguments ) {
        $database = self::getDatabase();
        if ( 1 == count( $arguments ) ) {
            $result = $database->max( self::getTableName(), $arguments[0] );
        } elseif ( 2 == count($arguments) ) {
            $result = $database->max( self::getTableName(), $arguments[0], $arguments[1] );
        } elseif ( 3 == count( $arguments) ) {
            $result = $database->max( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        return $result;
    }

    /**
     *
     * @param type $arguments
     * @return type
     * @throws ErrorException
     * @access public
     * @static
     */
    public static function minimum( ...$arguments ) {
        $database = self::getDatabase();
        if ( 1 == count( $arguments ) ) {
            $result = $database->min( self::getTableName(), $arguments[0] );
        } elseif ( 2 == count($arguments) ) {
            $result = $database->min( self::getTableName(), $arguments[0], $arguments[1] );
        } elseif ( 3 == count( $arguments) ) {
            $result = $database->min( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        return $result;
    }

    /**
     *
     * @param type $arguments
     * @return type
     * @throws ErrorException
     * @access public
     * @static
     */
    public static function average( ...$arguments ) {
        $database = self::getDatabase();
        if ( 1 == count( $arguments ) ) {
            $result = $database->avg( self::getTableName(), $arguments[0] );
        } elseif ( 2 == count($arguments) ) {
            $result = $database->avg( self::getTableName(), $arguments[0], $arguments[1] );
        } elseif ( 3 == count( $arguments) ) {
            $result = $database->avg( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        return $result;
    }

    /**
     *
     * @param type $arguments
     * @return type
     * @throws ErrorException
     * @access public
     * @static
     */
    public static function sum( ...$arguments ) {
        $database = self::getDatabase();
        if ( 1 == count( $arguments ) ) {
            $result = $database->sum( self::getTableName(), $arguments[0] );
        } elseif ( 2 == count($arguments) ) {
            $result = $database->sum( self::getTableName(), $arguments[0], $arguments[1] );
        } elseif ( 3 == count( $arguments) ) {
            $result = $database->sum( self::getTableName(), $arguments[0], $arguments[1], $arguments[2] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        return $result;
    }

    /**
     * get interceptor
     * @param string $name Column name
     * @access public
     */
    public function __get( $name ) {
        return $this->getAttribute( $name );
    }

    /**
     * set interceptor
     * @param string $name Column name
     * @param string $value Value
     * @access public
     */
    public function __set( $name, $value ) {
        $this->setAttribute( $name, $value );
    }

    public function  __unset( $name ) {
        unset( $this->attributes[$name] );
        unset( $this->updates[$name] );
    }

    public function  __isset( $name ) {
        return isset( $this->attributes[$name] );
    }
    
    protected function getAttribute( $name ) {
        $methodName = 'get' . ucfirst( str_replace( '_', '', $name ) );
        if ( method_exists( $this, $methodName ) ) {
            return $this->$methodName();
        } else {
            return $this->attributes[$name] ?? null;
        }
    }
    
    protected function setAttribute( $name, $value ) {
        $methodName = 'set' . ucfirst( str_replace( '_', '', $name ) );
        if ( method_exists( $this, $methodName ) ) {
            $this->$methodName( $value );
        } else {
            if ( array_key_exists( $name, $this->attributes ) && ( $this->attributes[$name] != $value ) ) {
                $this->updates[$name] = true;
            }
            $this->attributes[$name] = $value;
        }
    }
    
    /**
     * Returns a copy of the model's attributes hash.
     * @return array
     * @access public
     */
    public function getAttributes() {
        return $this->attributes;
    }
    
    /**
     * Whether the record is new and should be inserted
     * @return boolean
     * @access public
     */
    public function isNewRecord() {
        return $this->newRecord;
    }
    
    /**
     * Return true if any of the attributes has unsaved changed, false otherwise.
     * @return boolean
     * @access public
     */
    public function isChanged() {
        return count( $this->updates ) > 0;
    }
    
    /**
     * Whether the specific attribute changed
     * @param string $name
     * @return boolean
     */
    public function isAttributeChanged( $name ) {
        return array_key_exists( $name, $this->updates );
    }
    
    /**
     * Returns a hash of changed attributes indicating their new values
     * @return array
     * @access public
     */
    public function getChangedAttributes() {
        return array_intersect_key( $this->attributes, $this->updates );
    } 

    /**
     * Saves model data
     * @param boolean $validate
     * @return boolean True if the data has been successfully saved, false otherwise.
     * @access public
     */
    public function save( $validate = true ) {
        if ( $validate && $this->isInvalid() ) {
            return false;
        }
        
        IoC::resolve( 'eventmediator' )->trigger( 'before_save', [$this] );
        $this->notify( 'before_save' );
        $this->setTimestamps();
        $result = true;
        if ( $this->notify( 'save' ) ) {
            if ( $this->isNewRecord() ) {
                $result = $this->insert();
            } else {
                $result = $this->update();
            }
        }
        $this->notify( 'after_save' );
        IoC::resolve( 'eventmediator' )->trigger( 'after_save', [$this] );
        return $result;
    }

    /**
     * Inserts a single row.
     * @return boolean True if the data has been successfully inserted, false otherwise.
     * @access protected
     * @final
     */
    protected final function insert() {
        $database = self::getDatabase();
        IoC::resolve( 'eventmediator' )->trigger( 'before_insert', [$this] );
        $this->notify( 'before_insert' );
        $this->beforeInsert();
        $this->beforeSave();

        if ( $this->notify( 'insert' ) ) {
            $database->insert( self::getTableName(), $this->attributes );
            if ( DEVELOPMENT_ENVIRONMENT ) {
                IoC::resolve( 'logger' )->write( sprintf( '[%s] [%s] [%s] %s', date( 'Y-m-d H:i:s' ), __FILE__, 'debug', $database->last() ) );
            }
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        $this->attributes[self::getPrimaryKey()] = $database->id();
        $this->afterSave();
        $this->afterInsert();
        $this->notify( 'after_insert' );
        IoC::resolve( 'eventmediator' )->trigger( 'after_insert', [$this] );
        return true;
    }

    /**
     * Updates a single row.
     * @return boolean True if the data has been successfully updated, false otherwise.
     * @access protected
     * @final
     */
    protected final function update() {
        $database = self::getDatabase();
        IoC::resolve( 'eventmediator' )->trigger( 'before_update', [$this] );
        $this->notify( 'before_update' );
        $this->beforeUpdate();
        $this->beforeSave();

        if ( $this->notify( 'update') ) {
            if ( $this->isChanged() ) {
                $database->update( self::getTableName(), array_intersect_key( $this->attributes, $this->updates ), [self::getPrimaryKey() => $this->attributes[self::getPrimaryKey()]] );
                if ( DEVELOPMENT_ENVIRONMENT ) {
                    IoC::resolve( 'logger' )->write( sprintf( '[%s] [%s] [%s] %s', date( 'Y-m-d H:i:s' ), __FILE__, 'debug', $database->last() ) );
                }
            }
        }

        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }

        $this->afterSave();
        $this->afterUpdate();
        $this->notify( 'after_update' );
        IoC::resolve( 'eventmediator' )->trigger( 'after_update', [$this] );
        return true;
    }

    /**
     * Updates a model's timestamps.
     * @access protected
     */
    protected function setTimestamps() {
        if ( self::$recordTimestamps ) {
            $columns = self::getColumnNames();
            if ( $this->isChanged() && in_array( 'updated_at', $columns ) ) {
                $this->updated_at = date( 'Y-m-d H:i:s' );
            }
            if ( $this->isNewRecord() && in_array( 'created_at', $columns ) ) {
                $this->created_at = date( 'Y-m-d H:i:s' );
            }
        }
    }
    
    /**
     * Delete the model
     * @return boolean True if the model has been successfully deleted, false otherwise.
     * @access public
     */
    public function delete() {
        $database = self::getDatabase();
        IoC::resolve( 'eventmediator' )->trigger( 'before_delete', [$this] );
        $this->notify( 'before_delete' );
        $this->beforeDelete();
        
        if ( $this->notify( 'delete' ) ) {
            $primaryKey = self::getPrimaryKey();
            $database->delete( self::getTableName(), [$primaryKey => $this->attributes[$primaryKey]] );
        }
        $error = $database->error;
        if ( $error ) {
            throw new ErrorException( $error );
        }
        
        $this->afterDelete();
        $this->notify( 'after_delete' );
        IoC::resolve( 'eventmediator' )->trigger( 'after_delete', [$this] );
        return true;
    }
    
    /**
     * Assigns to $name the boolean opposite of $name
     * @param string $name
     * @access public
     */
    public function toggle( $name ) {
        $this->$name = !$this->$name;
        $this->save( false );
    }
    
    /**
     * Initializes $name to zero if null and adds the value passed as $by (default is 1).
     * @param string $name
     * @access public
     */
    public function increment( $name, $by = 1 ) {
        if ( !isset( $this->$name ) ) {
            $this->$name = 0;
        }
        $this->$name += $by;
        $this->save( false );
    }
    
    /**
     * Initializes $name to zero if null and substracts the value passed as $by (default is 1).
     * @param string $name
     * @access public
     */
    public function decrement( $name, $by = 1 ) {
        if ( !isset( $this->$name ) ) {
            $this->$name = 0;
        }
        $this->$name -= $by;
        $this->save( false );
    }
    
    /**
     * Executed every time the record gets validated.
     * This method is overriden in subclasses.
     * @access protected
     */
    protected function validate() {
        
    }
    
    /**
     * Executed when the record is new and gets validated.
     * This method is overriden in subclasses.
     * @access protected
     */
    protected function validateOnInsert() {

    }

    /**
     * Executed when the record is not new and gets validated.
     * This method is overriden in subclasses.
     * @access protected
     */
    protected function validateOnUpdate() {

    }

    /**
     * 
     * @param string $name Attribute name
     * @param string $message
     * @access public
     */
    public function addError( $name, $message ) {
        if ( array_key_exists( $name, $this->errors ) ) {
            $this->errors[$name][] = $message;
        } else {
            $this->errors[$name] = [$message];
        }
    }
    
    /**
     * 
     * @return boolean
     */
    public function isValid() {
        $this->notify( 'before_validate' );
        $this->beforeValidate();
        $this->errors = [];
        if ( $this->notify( 'validate' ) ) {
            $this->validate();
            if ( $this->isNewRecord() ) {
                $this->validateOnInsert();
            } else {
                $this->validateOnUpdate();
            }
        }
        $this->afterValidate();
        $this->notify( 'after_validate' );
        return ( 0 == count( $this->errors ) );
    }
    
    /**
     * 
     * @return boolean
     */
    public function isInvalid() {
        return !$this->isValid();
    }
    
    /**
     * 
     * @return array
     */
    public function getErrors() {
        return $this->errors;
    }
    
    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateNotEmpty( $name, $message ) {
        if ( !Validator::validateNotEmpty( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateLettersOnly( $name, $message ) {
        if ( !Validator::validateLettersOnly( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateAlphanumeric( $name, $message ) {
        if ( !Validator::validateAlphanumeric( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateMaxLength( $name, $max, $message ) {
        if ( !Validator::validateMaxLength( $this->$name, $max ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateMinLength( $name, $min, $message ) {
        if ( !Validator::validateMinLength( $this->$name, $min ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateNumber( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateNumber( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateInteger( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateInteger( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateMin( $name, $min, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateMin( $this->$name, $min ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateMax( $name, $max, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateMax( $this->$name, $max ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateDate( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateDate( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateTime( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateTime( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateURL( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateURL( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateEmail( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateEmail( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateIP( $name, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateIP( $this->$name ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateEqual( $name, $value, $message ) {
        if ( !Validator::validateEqual( $this->$name, $value ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateInArray( $name, $array, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateInArray( $this->$name, $array ) ) {
            $this->addError( $name, $message );
        }
    }

    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateRegExpr( $name, $pattern, $message ) {
        if ( !empty( $this->$name ) && !Validator::validateRegExpr( $this->$name, $pattern ) ) {
            $this->addError( $name, $message );
        }
    }
    
    /**
     * @param string $name Attribute name
     * @param string $message
     * @access protected
     */
    protected function validateUnique( $name, $message ) {
        if ( $this->exists( [$name => $this->$name] ) ) {
            $this->addError( $name, $message );
        }
    }
    
    public function jsonSerialize() {
        return $this->attributes;
    }

    /**
     * @access protected
     */
    protected function afterFind() {}

    /**
     * Executed before the row is inserted.
     * @access protected
     */
    protected function beforeInsert() {}

    /**
     * Executed after the row is inserted.
     * @access protected
     */
    protected function afterInsert() {}

    /**
     * @access protected
     */
    protected function beforeValidate() {}

    /**
     * @access protected
     */
    protected function afterValidate() {}

    /**
     * Executed before the row is updated.
     * @access protected
     */
    protected function beforeUpdate() {}

    /**
     * Executed after the row is updated.
     * @access protected
     */
    protected function afterUpdate() {}

    /**
     * Executed before the data is saved.
     * @access protected
     */
    protected function beforeSave() {}

    /**
     * Executed after the data is saved.
     * @access protected
     */
    protected function afterSave() {}

    /**
     * Executed before the row is deleted.
     * @access protected
     */
    protected function beforeDelete() {}

    /**
     * Executed after the row is deleted.
     * @access protected
     */
    protected function afterDelete() {}

    /**
     * Returns a string that represents the current object.
     * @return string A string representing the current object
     */
    public function __toString() {}
}
