0
\$\begingroup\$

An Object-relational mapper built using PHP.

This question is related to this old question. (Object relational mapper - optimize in memory data representation and code improvements)

This was done for educational purposes, and curious to know what kind of improvement has to be done to use in production.**
Please refer to the complete source code in the Github repository - (https://github.com/maleeshagimshan98/infinite-simple-orm)

Below, I've described the internal workings...

  • Entity Manager Class

    • This is the main instance we are interacting with.

    • Use it to retrieve and Create/Update data

    • It uses Entity objects and their attributes to build the query and execute the query.

    • Handles associations with other Entities using entity attributes ( $Entity->associations->get())

    • Uses ChangeTracker class to track changes made to retrieved objects and update them on the database accordingly.

  • Entity Class

    • Represent an entity in the database.

    • Has attributes of entity and column mappings to attributes for respective table

    • Has associations info that Entity has.

  • Entity class structure is here,

    • Entity class
      • associations are managed using AssociatedEntityContainer class.
      • attribs - attribute to column mapping, managed by AttributeMapContainer class

AttributeMapContainer Class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * Entity's Attribute Map Container Class
 */

 namespace Infinite\DataMapper\Entity;
 /**
  * Associated entity container
  */
 class AttributeMapContainer
 {

   /**
    * entity attributes map - [ 'attribute' => 'column_name']
    *
    * @var array
    */
   protected $attrib_map = [];

   /**
    * inverse of attribute mapping to column
    *
    * @var array
    */
   protected $attrib_map_inverse = [];

   /**
    * entity's primary keys
    *
    * @var array
    */
   protected $primary = [];

   /**
    * entity's properties (eg - autoIncrement)
    *
    * @var array
    */
   protected $properties = [];

   /**
    * read-only properties in entity (eg - AUTO INCREMENTED values)
    *
    * @var array
    */
   protected $readOnlyProps = [
      "autoIncrement"
   ];

   /**
    * constructor
    */
     public function __construct ()
     {        
     }

    /**
    * get a single attribute_map element
    *
    * @param string $attrib attrib_map element name
    * @return string attribute_map element
    * @throws \Exception
     */
    protected function map ($attrib = null) 
    {      
      if (!isset($this->attrib_map[$attrib])) {
         throw new \Exception("Undefined_Attribute");
      }
      return $this->attrib_map[$attrib];
    }

    /**
     * get entity's primary keys
     *
     * @return array
     */
    public function primary () {
       return $this->primary;
    }

    /**
    * set primary key
    *
    * @param object $attrib
    * @return void
    */
   protected function setPrimaryKey ($key)
   {//echo \json_encode($key);
      if ($key->primary !== false)
      {
         $this->primary[] = $key->primary;
      }
   }

   /**
    * set entity's properties
    *
    * @param object $properties object containing properties of entity attribute
    * @return void
    */
   protected function setProperties ($properties)
    {
       if (isset($properties["autoIncrement"])) {
          $this->properties["autoIncrement"][] = $properties["autoIncrement"];
       }
    }

    /**
     * get entity attribute's properties
     *
     * @param string $name property name (eg - autoIncrement)
     * @return array
     * @throws \Exception
     */
    public function getProperties ($name)
    {
       if (!isset($this->properties[$name])) {
          throw new \Exception("Undefined_Property");
       }
       return $this->properties[$name];
    }

    /**
     * sets the attribute's name with respect of configuration options
     *
     * @param string $attribName attribute name entity
     * @param string|object $attrib attribute options (if any), or (similar as $attribName)
     * @return object
     */
    protected function parseAttribute ($attribName,$attrib)
    {
       $result = (object) ["name" => "", "primary" => false, "properties" => []];

       //if property is an association
       if ($attribName === "_assoc") {
         $result =  false;
         return $result;
      }

       if (\is_object($attrib))
       {          
          if (!empty($attrib->name))
          {
             $result->name = $attrib->name;
          }
          if (isset($attrib->primary) && $attrib->primary == true)
          { //attribs with primary key, but no name comes here            
             $result->primary = $attrib->name ?? $attribName;
             $result->name = $attrib->name ?? $attribName;
          }
          if (isset($attrib->autoIncrement) && $attrib->autoIncrement == true)
          {
            $result->properties["autoIncrement"] = $attrib->name ?? $attribName;
          }
       }
       elseif (is_string($attrib))
       {
          $result->name =  $attrib;
       }
       return $result;       
    }

    /**
     * set entity's attributes
     *
     * @param string|object $attrib
     * @return void
     */
    public function init ($attrib)
    {
      foreach ($attrib as $key => $values) {
         $attribute = $this->parseAttribute($key,$values);

         if ($attribute) {            
            //setting the attribute mapping to columns
            $this->set($key,$attribute->name);
            $this->setPrimaryKey($attribute);

            if (\count($attribute->properties) > 0) {
               $this->setProperties($attribute->properties);
            }         
         }
      }
    }

    /**
     * get writable attributes of entity, mapped to columns 
     *
     * @return array
     */
    public function getWritableAttribsMapped ()
    {
       try {
      $readOnly = [];
      foreach ($this->readOnlyProps as $key) {
         $readOnly = array_merge($readOnly,$this->getProperties($key));         
       }
       return array_diff($this->getMapped(),$readOnly);
      }
      catch (\Exception $e) {
         return $this->getMapped();
      }
    }

    /**
     * get column names of entity attributes
     *
     * @param mixed $attribute
     * @return string|array
     * @throws \Exception
     */
    public function getMapped ($attribute = null)
    {
       if (empty($attribute))
         {
            return array_values($this->attrib_map);
         } 
      if (is_array($attribute))
      {
         $arr = [];
         foreach ($attribute as $key) {                         
            $arr[] = $this->map($key);
         }
         return $arr;
      }
      return $this->map($attribute); /** if $attribute === String */            
   }

   /**
    * map entity attributes of column names
    *
    * @param string $attribute
    * @return string|array
    * @throws \Exception
    */
   protected function mapAttribs ($attribute)
   {
      if ($attribute === null) {
         return array_keys($this->attrib_map_inverse); //check
      }

      if (!is_null($attribute) && !isset($this->attrib_map_inverse[$attribute])) {
         throw new \Exception("Undefined_Attribute");         
      }

      else {
         $attrib;
         foreach ($this->attrib_map_inverse as $key => $val) { //PUT THIS LOGIC INTO THE COMMON CLASS
            if ($key === $attribute) {
               $attrib = $val;
               break;
            }
         }
         return $attrib;
      }
   }

   /**
    * get entity's attributes
    *
    * @param mixed $attribute attribute name/s
    * @return array
    */
   public function getAttribs ($attribute = null)
   {
      if (\is_array($attribute)) {
         $arr = [];
         foreach($attribute as $key) {
            $arr[] = $this->mapAttribs($key);            
         }
         return $arr;
      }
      return $this->mapAttribs($attribute);      
   }

    /**
     * set entity attributes and mapping column names
     *
     * @param string $name
     * @param string $attrib
     * @return void
     */
    public function set ($name,$attrib)
    {
       $this->attrib_map[$name] = $attrib;
       $this->setInverseMap($attrib,$name);         
    }

    /**
     * set attrb_map's inverse
     *
     * @param string $name
     * @param string $attrib
     * @return void
     */
    protected function setInverseMap ($name,$attrib) 
    {
       $this->attrib_map_inverse[$name] = $attrib;
    }

    /**
     * get attrib_map's inverse
     *
     * @return array
     */
    public function getInverseMap ()
    {
       return $this->attrib_map_inverse;
    }
 }

AssociatedEntityContainer Class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * Associated Entity Container Class
 */

 namespace Infinite\DataMapper\Entity;
 /**
  * Associated entity container
  */
 class AssociatedEntityContainer 
 {

    /**
     * entity association data
     *
     * @var array
     */
    protected $associations = [];

    protected $associationKeys = [];

    /**
     * currently associated entities on this entity
     *
     * @var array
     */
    protected $currentAssociated = [];

    /**
     * constructor
     */
     public function __construct ()
     {

     }

     /**
      * get an association
      *
      * @param string $association
      * @return string
      * @throws \Exception
      */
     public function get ($association = null)
     {
        if (empty($association))
        {
            return $this->associations;
        }     
         if (!isset($this->associations[$association]))
         {
            throw new \Exception("Undefined_Association");
         }  
         return $this->associations[$association];
     }

     /**
      * get associations key names
      *
      * @return array
      */
     public function getAssociationKeys () 
     {
        return $this->associationKeys;
     }

     /**
      * set an association
      *
      * @param object $data
      * @return void
      * @throws \Exception
      */
     public function set ($data)
     {        
        foreach ($data as $key => $value)
        {
           $this->associationKeys[] = $key;            
            if (empty($value->target))
            {
               throw new \Exception("Target_Entity_Undefined");
               return; //IMPORTANT CHECK WHAT HAPPENS IN CASE OF ERROR
            }            
            $this->associations[$key] = $value;
        }           
    }
 }

 ?>

Entity Class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * base class for defining entities
 * in the database
 */

 namespace Infinite\DataMapper;

 use Infinite\DataMapper\Entity\AttributeMapContainer;
 use Infinite\DataMapper\Entity\AssociatedEntityContainer;
 use Infinite\DataMapper\Entity\EntityResult;

 /**
  * Entity class
  * represents an entity
  */
 class Entity {
    protected $qb;

    /**
     * entity name
     *
     * @var string
     */
    public $entityName;

    /**
     * entity's attributes
     *
     * @var Infinite\DataMapper\Entity\AttributeContainer
     */
    public $attribs;

    /**
     * entity's property names
     *
     * @var Infinite\DataMapper\Entity\EntityResult object
     */
    protected $entityResult;

    /**
     * entity table's name
     *
     * @var string
     */
    protected $entity_table = "";

    /**
     * entity associations with other entities
     *
     * @var Infinite\DataMapper\Entity\AssociatedEntityContainer
     */
    public $associations;
    /**
     * entity's data limit - for pagination
     *
     * @var integer
     */
    protected $limit = 10;

    /**
     * constructor
     * 
     * @param string $name database table name of the entity    
     * @param array|object $attribs
     * @return void
     * @throws \Exceptions
     */
    public function __construct ($attribs,$name)
    {
       if (!$attribs) {
          throw new \Exception("Invalid_Entity_Definition");
      }
       $this->attribs = new AttributeMapContainer();
       $this->associations = new AssociatedEntityContainer();
       $this->entity_table = $name;
       $this->init($attribs,$name);
    }

    /**
     * get entity table
     *
     * @return string
     */
    public function table () : string
    {
      return $this->entityTable;
    }

    /**
     * get entity's name
     *
     * @return string
     */
    public function name () 
    {
       return $this->entityName;
    }

   /**
    * get primary key
    *
    * @return string|array
    */
   public function primary ()
   { 
      return $this->attribs->primary();
   }

   /**
    * initialize entity's properties
    *
    * @param string|array $prop
    * @return void
    */
   public function initProps ($prop)
   {
      if (!$prop)
      {
         return;
      }
      if (is_array($prop))
      {
         foreach ($prop as $key)
         {
            $this->$key = null;
         }
         return;
      }
      $this->$prop = null;      
   }

    /**
     * validate data with defined data types
     * 
     * @param  mixed - data
     * @return boolean
     */
    public function validate ($props)
    {

    }

    /**
     * map entity attributes from configuration
     *
     * @param object $attribs entity attributes
     * @param string $name entity table name
     * @return AttributeMapContainer attribute map container
     */
    protected function init ($attribs,$name) : AttributeMapContainer
    { /* TESTING */   //echo \json_encode($attribs);
       $keys = [];
       
      if ($this->entity_table == "")
      {
         $this->entity_table = $name;
      }

      $this->attribs->init($attribs);

      if (isset($attribs->_assoc)) {
         $this->associations->set($attribs->_assoc);
      }
       
       //$this->initEntityResult();       
       return $this->attribs;
      }
      
      /**
       * initialize EntityResult object
       *
       * @return void
       */
      protected function initEntityResult ()
      {  
         $keys = $this->attribs->getAttribs();
         $keys = array_merge($keys,$this->associations->getAssociationKeys());
         return new EntityResult($keys,$this->entityName);   //always return a new instance      
      }

      /**
       * get entity result object
       *
       * @return Infinite\DataMapper\Entity\EntityResult object
       */
      public function getEntityResult ()
      {
         return $this->initEntityResult();
      }

    /**
     * format column names with table name
     *
     * @param string|array $columns
     * @return mixed
     */
    protected function withTable ($columns)
    {
       if (is_string($columns))
       {
          return $this->entityTable.".".$columns;
       }

       if (is_array($columns))
       {
          $formattedColumns = [];
          foreach ($columns as $key)
          {
             $formattedColumns[] = $this->entityTable.".".$key;
          }
          return $formattedColumns;
       }
    }

    /**
     * get entity attributes' column names (any or all)
     *     
     * @param string|array $attributeName
     * @return string|array
     */
    public function mapAttribsToColumn ($attributeName = null)
    {
       if (\is_null($attributeName))
       {
          return $this->withTable($this->attribs->getMapped());
       }      
       
       if (is_array($attributeName))
       {
         $columns = [];
         foreach ($attributeName as $val)
         {
            $columns[] = $this->withTable($this->attribs->getMapped($val));
         }
         return $columns;
       }

       return $this->withTable($this->attribs->getMapped($attributeName)); /* $attributeName === String */
    }

    /**
     * map column names to entity attributes
     *
     * @param mixed $data
     * @param EntityResult $entityResult
     * @return EntityResult entity result object
     */
    protected function mapColumnsToAttribs ($data,$entityResult)
    {       
       $inverseMap = $this->attribs->getInverseMap();
       
       foreach ($inverseMap as $key => $val) {
          $entityResult->$val = $data->$key;                              
       }
       return $entityResult;
    }

    /**
     * hydrate entity with the data fetched from database
     *
     * @param array|object $data  fetched data from database
     * @return self
     */
    public function hydrate ($data)
    { 
       $data = (object) $data;
       $arr = [];
       $entityResult = $this->initEntityResult();  //always returns a new instance
       return $this->mapColumnsToAttribs($data,$entityResult);
    }

    /**
     * set a field value
     *
     * @param string $attribute attribute name
     * @param mixed $value attribute's value
     * @return void
     * @throws \Exception
     */
    public function set ($attribute,$value)
    {
       /** Throws \Exception if attribute not found */
       $this->attribs->getAttrib($attribute);
       $this->$attribute = $value;
    }

    /**
     * get an indexed array of data for insertion, based on entity's atrributes
     * (only entity owned attributes)
     * returns an indexed array containing values
     *
     * @param [type] $data
     * @return array
     */
    public function insertProps ($entity)
    {
       $arr = [];

       $attribs = $this->attribs->getWritableAttribsMapped();
       
       foreach ($attribs as $key)
       {
          $attrib = $this->attribs->getAttribs($key);
          $arr[] = $entity->$attrib;
       }
       return $arr;
    }
 }
?>

Entity Manager

  • Entity Manager class structure
    • EntityContainer class - containing all entities.
    • queryDb class - for querying database.
    • QueryBuilderWrapper class - wrapping Enmvs\FluentPdo\Query query bulder class.

EntityContainer Class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * Entity Container Class
 */

 namespace Infinite\DataMapper;
 /**
  * Entity container
  */
 class EntityContainer 
 {

    /**
     * full qualified class name for the entity
     *
     * @var string
     */
    protected $base_class_name = "\Infinite\DataMapper\Entity\\";

    /**
     * database schema
     *
     * @var object
     */
    protected $dbSchema;

    /**
     * entities - [...,\Infinite\DataMapper\Entity\ENTITY_CLASS_NAME]
     *
     * @var array
     */
    protected $entities = [];


     /**
      * constructor
      */
     public function __construct ($entityDefinition = null)
     {
         if(!empty($entityDefinition))
         {
             $this->parseEntityDefinition($entityDefinition);
         }
     }

//...     

     /**
      * create an entity on the go
      *
      * @param string $name entity name
      * @return \Infinite\DataMapper\Entity entity object
      */
     public function entity ($name)
     {
         $entityDef = $this->getDefinition($name);
         if (!$entityDef)
         {
             throw new \Exception("Undefined_Entity");
         }
         $entityClass = $this->base_class_name."$name";
         return $this->entities[$name] = new $entityClass($entityDef,$name);         
     }

     /**
     * parse database entity definition
     *
     * @param [type] $schema JSON object defining the db schema
     * @return void
     */
    protected function parseEntityDefinition ($schema)
    {
        $arr = [];
        foreach ($schema as $key => $val) {
            $arr[$key] = $val;
        }
        $this->dbSchema = (object) $arr;
    }

    /**
     * get entity definitions
     *
     * @param string $name
     * @return mixed
     */
    protected function getDefinition ($name)
    { //echo json_encode($name);
        return $this->dbSchema->$name ?? false;
    }

    /**
     * checks if current entities count is zero
     *
     * @return boolean
     */
    public function is_empty () 
    {
        return count($this->entities) == 0;
    }


     /**
      * Get an entity
      *
      * @param string $entity Entity name
      * @return \Infinite\DataMapper\Entity object
      * @throws \Exception
      */
     public function get ($entity)
     {
        if (empty($this->entities[$entity])) {

            return $this->entity($entity);
            //throw new \Exception("Undefined_Entity");
        }
        return $this->entities[$entity];
     }

     /**
      * get all entities
      *
      * @return array
      */
     public function getAll () 
     {
         return $this->entities;
     }

     /**
      * set entity
      *
      * @param string $name
      * @param Infinite\DataMapper\Entity $entity entity object
      * @return void
      */
     public function set ($name,$entity)
     {
         $this->entities[$name] = $entity;                    
     }
 }

 ?>

For the context of this question other queryDb, QueryBuilderWrapper classes are not relevant, thus not shown here.

Entity Manager class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * Entity Manager class
 */

 namespace Infinite\DataMapper;

 include_once dirname(__DIR__)."/Products.php"; 
 include_once dirname(__DIR__)."/sku.php";
 include_once dirname(__DIR__)."/product_sku.php";
 include_once dirname(__DIR__)."/orders.php";

 use Infinite\DataMapper\EntityContainer;
 use Infinite\DataMapper\Entity\EntityResult;
 use Infinite\DataMapper\ChangeTracker;
 use Infinite\DataMapper\QueryBuilder\QueryBuilderWrapper;
 use Infinite\DataMapper\QueryDb;
 use Infinite\DataMapper\Pagination;
 
 /**
  * Entity manager class
  */
 class EntityManager {

    /**
     * PDOConnection
     *
     * @var \PDO object
     */
    private $connection;

    /**
     * Entities
     *
     * @var Infinite\DataMapper\EntityContainer 
     *
     */
    public $entities;

    /**
     * current entity
     *
     * @var Infinite\DataMapper\Entity
     */
    protected $currentEntity;

    /**
     * current associated entities
     *
     * @var Infinite\DataMapper\EntityContainer
     */
    protected $currentAssociated;

    /**
     * sql statement created when queriying entities
     *
     * @var \PDOStatement
     */
    protected $sqlStatement;

    /**
     * sql statements created when inserting data
     * have multiple sql statements
     * execute all in one transaction 
     *
     * @var array
     */
    protected $sqlStatementStack = [];

    /**
     * CRUD action - select,insert,update,delete
     *
     * @var string
     */
    protected $action = "";

    /**
     * constructor
     *
     * @param object $config
     * @return void
     */
    public function __construct (object $config)
    {        
        $this->initConfig($config);        
    }

    /**
     * initialize Entity manager with configuration
     *
     * @param object $config  configuration object
     * @return void
     * @throws \Exception
     */
    protected function initConfig (object $config)
    {
        if (empty($config->connection)) {
            throw new \Exception("Database_Connection_Not_Found");
        }
        
        $this->connection = $config->connection; //CONSIDER REMOVING THIS PROPERTY
        $this->entities = new EntityContainer(json_decode(file_get_contents(dirname(__DIR__)."/config/entity_definition.json")));
        $this->currentAssociated = new EntityContainer();
        $this->queryBuilder = new QueryBuilderWrapper($this->connection);
        $this->queryDb = new QueryDb($this->connection);
    }

    /**
     * return a new entity result object of a respective entity class - when inserting new data
     *
     * @param string $entity entity name
     * @return Infinite\DataMapper\Entity\EntityResult Entity result object
     * @throws \Exception
     */
    public function entity (string $entityName)
    {   
        return $this->entities->entity($entityName)->getEntityResult();        
    }

    /**
     * set current entity
     *
     * @param string $entity entity name
     * @return void
     */
    protected function setCurrentEntity ($entity)
    {
        $this->currentEntity = $this->entities->get($entity);
    }

    /**
     * clear current entity
     *
     * @return void
     */
    protected function clearCurrentEntity ()
    {
        $this->currentEntity = null;
    }

    /**
     * prepare basic parts of query string
     * for getting an entity from database
     *
     * @param \Infinite\DataMapper\Entity\ $entity entity object
     * @return void
     */
    protected function prepareGet ($entity)
    { /* TESING */ //echo \json_encode($entity);
        $this->sqlStatement = $this->queryBuilder->from($entity->table())
        ->select($entity->mapAttribsToColumn());
        return $this->sqlStatement;
    }

    /**
     * get entity from database
     *
     * @param string $entity entity name
     * @param array|null $id get a row based on id [column_name,value]
     * @return self
     * @throws \Exception
     */
    public function get ($entity,$id = null)
    {  
        $this->action = "select";
        $this->setCurrentEntity($entity); //echo json_encode($this->currentEntity);
        $sql = $this->prepareGet($this->currentEntity);
        if (!empty($id)) {
            $sql = $sql->where([
                $this->currentEntity->mapAttribsToColumn($id[0]) => $id[1]
            ]);        
        }
        return $this;
        /** TESTING */ //echo json_encode();
    }

    /**
     * associate entities in the result
     *
     * @param string $entity associated entity name
     * @param mixed  $joiningEntityId entity id (or foreign key) of associated entity
     * @return self
     * @throws \Exception
     */
    public function associate ($entity,$joiningEntityId = null)
    {
        $associated = $this->currentEntity->associations->get($entity);
        $target = $this->entities->get($associated->target);        
        $this->currentAssociated->set($associated->target,$target); //IMPORTANT - CHECK IF THIS IS NEEDED - $target
        
        $this->sqlStatement = $this->sqlStatement->leftJoin([
            $target->mapAttribsToColumn($associated->refer),
            $this->currentEntity->mapAttribsToColumn($associated->inverse)
            ])->select($target->mapAttribsToColumn());
        
            if (is_array($joiningEntityId))
            {
                $this->sqlStatement = $this->sqlStatement->where(
                    [
                        $target->mapAttribsToColumn($joiningEntityId[0]) => $joiningEntityId[1]
                    ]
                );
            }
            return $this;
    }
    
    /**
     * hydrate entity with data fetched from database
     *
     * @param array|object $res data fetched from database
     * @return array
     */
    protected function hydrate ($res) 
    {
        $this->tracker = new ChangeTracker();   
        if (!$res) {
            return $res;
        }

        $currentEntity = $this->currentEntity;
        $entityCollection = [];
        foreach ($res as $key) 
        {
            $result = $currentEntity->hydrate($key);
             //keep track of objects
            if (!$this->currentAssociated->is_empty())
            {
               $result = $this->hydrateAssociated($result,$key);
            }
            $this->tracker->addTracking($currentEntity->name(),$result);
            $entityCollection[] = $result;
        } //echo json_encode($entityCollection);
        return $entityCollection;
    }

    /**
     * hydrate associated entity of the current entity
     * (only single object from result set,
     * run this multiple times to hydrate all entities, and associated ones)
     *
     * @param Infinite\DataMapper\Entity\EntityResult $entity entiy result object
     * @param array|object $data data fetched from database
     * @return Infinite\DataMapper\Entity\EntityResult
     */
    protected function hydrateAssociated ($entity,$data) 
    {
        $associated = $this->currentAssociated->getAll(); 
        foreach ($associated as $key => $value) 
        {
            $assocEntityResult = $this->entities->entity($key)->hydrate($data);
            $entity->set($key,$assocEntityResult);
            //$this->tracker->addTracking($assoc->name(),$assoc);
        }
        return $entity;
    }

    /**
     * insert data to database - create an insert query
     *
     * @param EntityResult $entity entity result object
     * @param mixed $value values to be inserted
     * @return void
     */
    protected function insertData ($entity,$value = null)
    {
        $this->action = "insert";
        $this->setCurrentEntity($entity->name());
        if (isset($value)) {
            $entity->__setFromObj((object)$value); // $value must be an object
        }

        $writableAttribs = $this->currentEntity->attribs->getWritableAttribsMapped();
        $insertDataArr = $this->currentEntity->insertProps($entity);

        $sqlStatement[] = $this->queryBuilder->insert(
            $this->currentEntity->table(),
            $writableAttribs,
            $insertDataArr
        )->getSqlString(); //echo $sqlStatement[0];

        $sqlStatement[] = $insertDataArr;
        $this->sqlStatementStack[] = $sqlStatement;
        //ONLY MAIN ENTITY IS BEING INSERTED

    }

    /**
     * process primary keys to ypdate entities
     *
     * @param EntityResult $entityResult entity result object
     * @return array
     */
    protected function processPrimaryKeys ($entityResult)
    {
        $this->setCurrentEntity($entityResult->name());
        $primaryKey = $this->currentEntity->primary();
        $primaryKeyValues = [];
        $values = [];
        
        foreach ($primaryKey as $key) {
            $attrInverseMap = $this->currentEntity->attribs->getInverseMap();
            $primaryKeyMapped = $attrInverseMap[$key]; //check
            $primaryKeyValues[$key] = $entityResult->$primaryKeyMapped;
            $values[] = $entityResult->$primaryKeyMapped;
        }
        return ["pk" => $primaryKeyValues, "values" => $values];
    }

    /**
     * update entity values in database
     *
     * @param EntityResult $entity entity result object
     * @param array $changes - array with key => values of changes (keys have been mapped to column names)
     * @return void
     */
    protected function updateData ($changes,$primaryKeys)
    {
        $this->action = "update";       
        
        $sqlStatement[] = $this->queryBuilder->update(
            $this->currentEntity->table(),
            $changes,
            $primaryKeys["pk"]
        )->getSqlString();

        $sqlStatement[] = array_merge(array_values($changes),$primaryKeys["values"]);
        $this->sqlStatementStack[] = $sqlStatement;
    }

    /**
     * process changed properties of EntityResult to update
     *
     * @param EntityResult $entity entity result object
     * @param array $changes associated array with changes
     * @return void
     */
    protected function processChanges ($entity,$changes)
    {
        if (isset($changes["_assoc"])) {
            $assocChanges = $changes["_assoc"];
            $primaryChanges = array_diff($changes,$assocChanges);

            foreach ($assocChanges as $key => $value) {
                //IMPORTANT - ADD NECESSARY UPDATES FOR ASSOCIATED ENETITIES
                $this->updateData();//check
            }
        }
        else {
            $primaryChanges = $changes;
        }                
        $this->updateData($primaryChanges,$this->processPrimaryKeys($entity));
    }

    /**
     * save an entity to database
     * insert new row if new data, or update if retrieved entity has been changed
     *
     * @param EntityResult $entity entity object
     * @param array $values associated array of values - ["column_1" => "value", "column_2" => "value"]
     * @return void
     */
    public function save ($entity,$values = null)
    {        
        $firstState = $this->tracker->isTracked($entity); //check
        if ($firstState) {
            $this->setCurrentEntity($firstState->name());
            $assoc = $this->currentAssociated->getAll();
            $changes =  $this->tracker->getChanges($entity,$assoc,$this->entities);
            if (!$changes) {
                return; // if no changes, no need to save
            }            
            $this->processChanges($entity,$changes);
        }
        else  {
            $this->insertData($entity,$values);
            return $this;
        }        
        return $this;                
    }

    /**
     * delete an entity from database
     *
     * @return void
     */
    public function delete ()
    {

    }

    /**
     * execute the current CRUD Action statement
     *
     * @return void
     */
    public function go ($data = null)
    {
        if ($this->action === 'select')
        {
            $res = $this->queryDb->select((object) [
                "sql" => $this->sqlStatement->getSqlString(),
                "data" => $data
            ]);
            $this->currentAssociated->clear();
            /* TESTING */// echo $sql;
            /* TESTING */ //echo json_encode($res);
            return $this->hydrate($res); 
            //return $this->result($res);
        }
        if ($this->action === "insert")
        {
            $isDone = true; //CHECK
            $this->queryDb->transaction();
            foreach ($this->sqlStatementStack as $sql)
            {
                $isDone = $this->queryDb->insert((object)[
                    "sql" => $sql[0],
                    "data" => $sql[1]
                ]);
            }
            if ($isDone)
                {
                    $this->queryDb->commit(); //IMPORTANT - IF ONE OF QUERY FAILS, ALL WILL BE FAILED
                }
                else {
                    $this->queryDb->rollback();
                }
            return $isDone;     
        }

        if ($this->action === "update") {            
            return $this->queryDb->batchWrite($this->action,$this->sqlStatementStack);
        }
    }

EntityResult class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * Entity's result object
 */

 namespace Infinite\DataMapper\Entity;
 /**
  * Entity's result object class
  */
 class EntityResult
 {
    /**
     * timestamp of the creation of this instance
     *
     * @var integer
     */
    private $__createdAt;

   /**
    * name of entity of this object belong
    *
    * @var string
    */
   protected $entityName;

   /**
    * entity attributes
    *
    * @var array
    */
  protected $props = [];

   /**
    * constructor
    * @param array $props properties of entity
    */
     public function __construct ($props,$name)
     {
         $this->__createdAt = explode(".",microtime(true))[1]; 
         $this->entityName = $name;
         if ($this->is_assoc($props))
         {
            foreach ($props as $key => $value)
            {
                $this->props[] = $key;
                $this->$key = $value;
            }
         }
         else 
         {
             foreach ($props as $key)
             {
                $this->props[] = $key;
                 $this->$key = "";
             }
         }
         
     }

   final public function __getCreatedTime ()
   {
      return $this->__createdAt;
   }

   /**
    * return entity name of this object belongs
    *
    * @return string $entityName entity name
    */
   public function name ()
   {  
      return $this->entityName;
   }

     /**
      * check if an array is an associative array or not
      *
      * @param array $array
      * @return boolean
      */
    protected function is_assoc ($array)
    {
        foreach (array_keys($array) as $k => $v)
        {
          if ($k !== $v)
          {
            return true;
          }
        }
        return false;
      }

      /**
      * __call magic method - use to
      * dynamically create set and get methods
      *
      * @return mixed
      */
     public function __call ($name,$args)
     {         
         $functionName = strtolower($name);
         $isGetter = explode("get",$functionName);
         $isSetter = explode("set",$functionName);

         if (is_array($isGetter) && count($isGetter) > 1)
         {
             if ($this->props($isGetter[1]))
             {
                 return $this->$isGetter[1];
             }
             
         }

         if (\is_array($isSetter) && count($isSetter) > 1)
         {
            if ($this->props($isSetter[1]))
            {
                $this->set($isSetter[1],$args);
            }
         }

     }

     /**
      * clone this instance, to keep track of changes
      * clones properties even if it is an object
      *
      * @return void
      */
     public function __clone () {
        
        foreach ($this->props as $prop) {
           if (\is_object($this->$prop)) {
              $this->$prop = clone $this->$prop;
           }
        }
     }

     /**
     * get instance's property names as an associative array
     * 
     * @param object $data - object with data
     * @return array
     */
    public function getAssocArray()
    {
      $arr = [];
      foreach($this->props as $key)
      {
          if (isset($this->$key))
          {
              $arr[$key] = $this->$key;
          }
      }//echo json_encode($arr);
      return $arr;
   }

   /**
    * get entity result object's properties in an indexed array
    *
    * @return array
    */
   public function getPropsArray ()
   {
      $arr = [];
      foreach ($this->props as $key)
      {
         $arr[] = $this->$key;
      }
      return $arr;
   }


    /**
    * get a single attribute
    *
    * @param string $attrib
    * @return string attribute
    * @throws \Exception
     */
    protected function props ($prop = null) 
    {
      if (empty($prop)) {
         return $this->props;
      }

      if (!isset($this->$prop)) {
         throw new \Exception("Undefined_Property");
      }
      return $this->$prop;
    }

    /**
     * get entity's attributes
     *
     * @param mixed $attribute
     * @return string|array
     * @throws \Exception
     */
    public function get ($prop = null)
    {
       try
       {
         if (is_array($prop))
         {
            $arr = [];
            foreach ($prop as $key) {                         
               $arr[] = $this->props($key);
            }
            return $arr;
         }
         return $this->props($prop);
       }
      catch (\Exception $e)
      {
         if ($e->getMessage() == "Undefined_Property" ) //IMPORTANT - CONSIDER ADDING && $prop !== null
         {
            return $this->props();
         }
      }       
   }

    /**
     * set a property
     *
     * @param string $name property name
     * @param mixed $prop property value
     * @return void
     */
    public function set ($name,$prop)
    { 
       try {
          $this->props($name);
          $this->$name = $prop;
       }
       catch (\Exception $e) {
          if ($e->getMessage() === "Undefined_Property") {
             return;
          }
       }         
    }

    /**
     * set object proprties from an associative array
     *
     * @param array $array an associative array of object props and values
     * @return void
     */
    public function __setFromObj ($obj)
    {
       foreach ($this->props() as $key)
       {
          if (!isset($obj->$key))
          {
             continue; //CHECK
          }
          $this->$key = $obj->$key;
       }
    }
 }
 ?>

ChangeTracker class

<?php
/**
 * © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
 *
 * Changes tracker for EntityResult objects
 */

 namespace Infinite\DataMapper;

 use Infinite\DataMapper\Entity\EntityResult;

 /**
  * class for tracking changes in EntityResult object
  */
 class ChangeTracker 
 {
     /**
      * timestamps of creation time of EntityResult objects
      *
      * @var array
      */
     protected  $__createdTimes = [];

     /**
      * first state of EntityResult.
      * clone of actual EntityResult
      *
      * @var EntityResult
      */
     protected $currentFirstState;

     /**
      * detected changes of EntityResult and it's associations
      *
      * @var array
      */
     protected $changes = [];

     /**
      * EntityManager's current entity
      *
      * @var Infinite\DataMapper\Entity entity object
      */
     protected $entity;


     /**
      * constructor
      */
     public function __construct () {
     }

     /**
     * add tracking of entity result object
     *
     * @param string $currentEntityName name of current entity
     * @param EntityResult $result Entity result object
     * @return void
     */
    public function addTracking ($currentEntityName,$result)
    { 
        $this->__createdTimes[$currentEntityName][$result->__getCreatedTime()] = clone $result; //keep track of objects
    }

    /**
     * get the first state of stored EntityResult
     *
     * @return EntityResult
     */
    public function getFirstState ()
    {
        return $this->currentFirstState;
    }

    /**
     * set changed properties and values
     *
     * @param array $changes
     * @return void
     */
    protected function setChanges ($changes)
    {
        if ($changes) {
            $this->changes = array_merge($this->changes,$changes);
        }
    }

    /**
     * set changes association values
     *
     * @param string $key name of associated property
     * @param array $changes array of changed properties and values
     * @return void
     */
    protected function setChangedAssoc ($key,$changes)
    {
        $this->changes["_assoc"][$key] = $changes;        
    }

    /**
     * check if EntityResult object is a tracked one
     * and if tracked, return it's first state, cloned by entity manager
     *
     * @param EntityResult $entity entity result object
     * @return EntityResult|false
     */
    public function isTracked ($entity) 
    {
        $created = $entity->__getCreatedTime();
        $entityName = $entity->name();

        if (!isset($this->__createdTimes[$entityName])) { //IMPORTANT - TRY CHECKING LENGTH OF THIS ARRAY INSTEAD  OF ISSET
            return false;
        }
        if (isset($this->__createdTimes[$entityName][$created])) {
            $this->currentFirstState = $this->__createdTimes[$entityName][$created];
            return $this->__createdTimes[$entityName][$created];            
        }
    }

    /**
     * compare $this->currentFirstState's associations
     * by traversing through properties
     *
     * @param string $key key of $this->firstEntity
     * @param EntityResult $object object to be compared
     * @return array|boolean
     */
    protected function compareAssociations ($key,$object)
    {
        $changes = [];
        $entity = $this->entityContainer->get($object->name()); //associated entity

        /*
        * strictly only iterate over $this->currentFirstState
        */
        foreach ($this->currentFirstState->$key as $prop => $val) {
            //TRY CHECKING IF $object->$key is set, (BUT MAY BE EQUAL TO NULL)
            if ($object->$prop != $val) {
                $mapped = $entity->attribs->getMapped($prop);
                $changes[$mapped] = $object->$prop;
            }
        }
        return count($changes) > 0 ? $changes : false; //IF NO CHANGE DETECTED, RETURN FALSE
    }

    /**
     * compare firstState with current state of entity result
     *
     * @param string $key object key, which need to be checked
     * @param EntityResult $firstState first state of entity result
     * @param EntityResult $current current state of entity result
     * @return mixed
     */
    public function compareChanges ($key,$current)
    {
        $changes = [];
        //comparing object, to check if values changed
        //STRICTLY USE == operator        
        if (\is_object($current->$key) && ($this->currentFirstState->$key == $current->$key)) {
            return false;                      
        }
        //IMPORTANT, STRICT TYPE CHECKING
        if ($current->$key === $this->currentFirstState->$key) { 
            return false; //IF NO CHANGE DETECTED, RETURN FALSE
        }        
        else {
            $mapped = $this->entityContainer->get($this->currentFirstState->name())->attribs->getMapped($key);
            $changes[$mapped] =  $current->$key; //store value changes
            return $changes;
        }
    }

    /**
     * get changed properties in EntityResult
     *
     * @param EntityResult $firstState first state object copy of Entity Result
     * @param EntityResult $current current Entity Result
     * @return array|boolean
     * @throws \Exception
     */
    public function getChanges ($current,$associatedEntities,$entityContainer)
    {
        $this->entityContainer = $entityContainer;
        
        foreach ($this->currentFirstState as $key => $value) {

            if (array_search($key,$associatedEntities) !== false) { // if prop is  an associated entity,                    
                if (!($current->$key instanceof EntityResult)) {
                    throw new \Exception("Associated_Entity_Not_Instance_Of_EntityResult");
                }
                //greedy way, skip traversing if entity not changed
                if ($this->currentFirstState->$key == $current->$key) {
                    continue;
                }
                $this->setChangedAssoc($key,$this->compareAssociations($key,$current->$key));                                    
                continue;                
            }

            $this->setChanges($this->compareChanges($key,$current));
        }
        return count($this->changes) >0 ? $this->changes : false;
    }
}
 ?>

Reasons for posting this question and what I'm looking for

  • Any improvements in the in-memory representation of relationships, entity attributes. (data structure)
  • Mostly I'm bothered about using foreach loops in most places.
  • Any improvement for the code and for the code structure is welcome.
  • What kind of improvements to be done to use in production?
\$\endgroup\$
2
  • \$\begingroup\$ Mostly I'm bothered about using foreach loops in most places.” would you rather have a different loop style, or hope/expect to achieve the same functionality without a loop? \$\endgroup\$ Jul 4 at 11:22
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ For most parts, I hope to reduce the number of loops, or any other clever way of implementing functionality but also keeping the current performance or making it higher than present. \$\endgroup\$ Jul 4 at 12:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.