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
- associations are managed using
- Entity 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 - wrappingEnmvs\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?