polyfony-inc/polyfony

View on GitHub
Private/Polyfony/Entity/Aware.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php
 
namespace Polyfony\Entity;
use Polyfony\Exception as Exception;
use Polyfony\Query\Convert as Convert;
use Polyfony\Query as Query;
use Polyfony\Database as Database;
use Polyfony\Security as Security;

#[\AllowDynamicProperties]
class Aware {
    
    // storing variable that does not reflect the database table structure
    protected array $_;

    // columns that are to be auto-populated
    const AUTO_POPULATE_COLUMNS = [
        // column that are autopopulated upon creation
        'creation'        =>[
            'creation_by'        =>'id_account',
            'created_by'        =>'id_account',
            'creation_date'        =>'time',
            'creation_datetime'    =>'time',
            'created_at'        =>'time',
        ],
        // columns that are autopopulated upon modification
        'modification'    =>[
            'modification_by'    =>'id_account',
            'modified_by'        =>'id_account',
            'modification_date'    =>'time',
            'modified_at'        =>'time',
        ]
    ];

    // storing the validators
    const VALIDATORS = [];

    // storing the filters
    const FILTERS = [];

    // create a object from scratch, of fetch it in from its table/id
    public function __construct(
        $conditions_to_find_the_record = null
    ) {

        // init the list of altered columns
        $this->_ = [
            // id of the record
            'id'        => isset($this->id) ? $this->id : null,
            // table of the record
            'table'        => get_class($this) != 'Polyfony\Record' ? 
                str_replace('Models\\','',get_class($this)) : null,
            // list of altered columns since the retrieval from the database
            'altered'    => []
        ];
        // if conditions are provided
        if($conditions_to_find_the_record !== null) {
            // we instanciate ourself from an existing database record
            $this->__constructFromExistingRecord($conditions_to_find_the_record);
        }
        // return self
        return $this;
        
    }
    
    private function __constructFromExistingRecord($conditions) :void {

        // if conditions is not an array, we assume it is the id of the record
        $conditions = is_array($conditions) ? $conditions : ['id'=>$conditions];
        // grab that object from the database
        $record = self::_select()->where($conditions)->get();
        // if we didn't find the record
        if(!$record) {
            // throw a 404 Not found Exception
            Throw new Exception(
                "new Models\\{$this->_['table']} : Object not found in the database", 
                404
            );
        }
        // clone the found record
        $this->replicate($record);

    }

    private function replicate($clone) {
        // for each attribute
        foreach(get_object_vars($clone) as $attribute => $value) {
            // clone that attribute
            $this->{$attribute} = $value;
        }
        // replicate the id if it is available
        $this->_['id'] = isset($this->id) ? $this->id : $this->_['id'];
    }

    // magic
    public function __clone() {
        // set all columns as altered
        $this->_['altered'] = array_keys(get_object_vars($this));
        // remove the hidden column
        unset($this->_['altered'][0]);
        // remove the hidden id, so that the object is recognized as absent from the database
        $this->_['id'] = null;
        // remove the id attribute too
        $this->id = null;
    }

    private function alter(
        string $column
    ) {
        // push
        $this->_['altered'][] = $column;
        // deduplicate
        $this->_['altered'] = array_unique($this->_['altered']);
    }

    private function autoPopulate(
        string $when
    ) :void {
        
        // for each column to autopopulate
        foreach(
            self::AUTO_POPULATE_COLUMNS[$when] as 
            $column => $with_what
        ) {
            // if the column exists
            if(Database::doesColumnExist(
                $column, 
                $this->_['table']
            )) {
                
                if(
                    // if the column is to be autopopulated with the unix epoch
                    $with_what == 'time' && 
                    // and if it has not already been altered manually
                    !in_array($column, $this->_['altered'])
                ) {
                    $this->set([
                        $column=>time()
                    ]);
                }
                elseif(
                    // if the column is to be autopopulated with the current account's id
                    $with_what == 'id_account' && 
                    // and if we have a account id to provide
                    Security::isAuthenticated() && 
                    // and if it has not already been altered manually
                    !in_array($column, $this->_['altered'])
                ) {
                    $this->set([
                        $column=>Security::getAccount()->get('id')
                    ]);
                } 
            }
        }
    }

    public function get(
        string $column, 
        bool $get_it_raw = false
    ) {
        // return the columns or null if it does not exist        
        return 
            isset($this->{$column}) && 
            strlen($this->{$column}) ? 
                Convert::valueFromDatabase(
                    $column, 
                    $this->{$column}, 
                    $get_it_raw
                ) : 
                null;
    }

    public function set(
        $column_or_array, 
        $value = null
    ) {
        // if we want to set a batch of values
        if(is_array($column_or_array)) {
            // for each value to set
            foreach($column_or_array as $column => $value) {
                // set that individual column
                $this->set($column, $value);
            }
        }
        // setting only a single value
        else {
            // validate the value according to what we know (nulls, existing columns, const validators)
            Validator::isThisValueAcceptable(
                $this->_['table'], 
                get_class($this), 
                $column_or_array, 
                $value
            );
            // filter the value according to the models defined filters
            $value = Filter::sanitizeThisValue(
                $column_or_array, 
                get_class($this),
                $value
            );
            // convert the value depending on the column name and set it
            $converted_value_ready_for_updating = Convert::valueForDatabase(
                $column_or_array, 
                $value
            );
            // if there's actually something that has changed
            if(
                (
                    isset($this->{$column_or_array}) && 
                    $this->{$column_or_array} != $converted_value_ready_for_updating
                ) || !isset($this->{$column_or_array})
            ) {
                // update the value
                $this->{$column_or_array} = $converted_value_ready_for_updating;
                // update the altered list
                $this->alter($column_or_array);
            }
        }
        // return self
        return $this;
    }
    
    // update or create
    public function save() :bool {
        
        // if nothing has changed on this object
        if(!count($this->_['altered'])) {
            // we consider the object saved without touching the database
            return true;
        }

        // if an id already exists
        if($this->_['id']) {
            // autopopulate
            $this->autoPopulate('modification');
            // we can update and return the number of affected rows (0 on error, 1 on success)
            return (bool) self::_update()
                ->set(
                    $this->__toArray(
                        true, 
                        true
                    )
                )
                ->where(['id'=>$this->_['id']])
                ->execute();
        }
        // this is a new record
        else {
            // autopopulate both columnset
            $this->autoPopulate('creation');
            $this->autoPopulate('modification');
            // try to insert it
            $inserted_object = self::create(
                $this->__toArray(
                    true, 
                    true
                )
            );
            // if insertion succeeded, return true
            if($inserted_object) {
                // clone ourselves with what the database returneds, a full fledged object
                $this->replicate($inserted_object);
                // return success
                return true;
            }
            // didnt insert
            else {
                // failure feedback
                return false;
            }
        }
        
    }
    
    // delete
    public function delete() {
        // if id or table if missing
        if(
            !$this->_['table'] || 
            !$this->_['id']
        ) {
            // throw an exception
            Throw new Exception(
                get_class($this).
                '->delete() : cannot delete a record without table and id', 
                400
            );
        }
        // if it went well
        return (bool) self::_delete()
            ->where(['id'=>$this->_['id']])
            ->execute();
    }

    // checks if the object has been persisted to database
    public function exists() :bool {
        return !is_null($this->_['id']);
    }

    // returns the name of the class that has extended this one (aka, the Table name)
    protected static function tableName() :string {
        // removed the namespace from the class name
        return str_replace('Models\\','',get_called_class());
    }

    // magic
    public function __toArray(
        bool $raw = false, 
        bool $altered = false
    ) {
        // declare an empty array
        $array = [];
        // what to iterate on
        $attributes = $altered ? 
            $this->_['altered'] : 
            array_keys(get_object_vars($this));
        // for each attribute of this object
        foreach($attributes as $attribute){
            // if the attribute is not internal
            if($attribute != '_') {
                // convert or not
                $array[$attribute] = $raw ? 
                    $this->get($attribute,true) : 
                    $this->get($attribute,false);
            }
        }
        return $array;
    }

    // magic
    public function __toString() {
        // a string to sybolize this record
        return $this->_['id'] ? $this->_['id'] : 0;
    }

    // shortcut to insert an element
    public static function create(
        array $columns_and_values=[]
    ) {

        return Database::query()
            ->insert($columns_and_values)
            ->into(self::tableName())
            ->execute();

    }

    // shortcut that bootstraps a select query
    // PHP doesn't yet allow to have static method and object methods with the same name
    // that's why we have to prefix them if an underscore 
    public static function _select(
        array $select=[]
    ) :Query {

        // returns a Query object, to execute, or to complement with some more parameters
        return Database::query()
            ->select($select)
            ->from(self::tableName());

    }

    // shortcut that bootstraps an update query
    // PHP doesn't yet allow to have static method and object methods with the same name
    // that's why we have to prefix them if an underscore
    public static function _update() :Query {

        // returns a Query object, to execute, or to complement with some more parameters
        return Database::query()
            ->update(self::tableName());

    }

    // shortcut that bootstraps a delete query
    // PHP doesn't yet allow to have static method and object methods with the same name
    // that's why we have to prefix them if an underscore
    public static function _delete() :Query {

        // returns a Query object, to execute, or to complement with some more parameters
        return Database::query()
            ->delete()
            ->from(self::tableName());

    }

}


?>