An Object-relational mapper built using php.
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()
)
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
- attributes are managed using
AttributeContainer
class. - associations are managed using
AssociatedEntityContainer
class. - attribs_map - attribute to column mapping, managed by
AttributeMapContainer
class
- attributes are managed using
- Entity class
AttributeContainer Class
<?php
/**
* © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
*
* Entity's Attribute Container Class
*/
namespace Infinite\DataMapper\Entity;
/**
* Attribute container
*/
class AttributeContainer
{
/**
* entity attributes
*
* @var array
*/
protected $attribs = [];
/**
* entity property names
*
* @var array
*/
protected $props = [];
/**
* constructor
*/
public function __construct ()
{
}
/**
* get a single attribute
*
* @param string $attrib
* @return string attribute
* @throws \Exception
*/
protected function attribute ($attrib = null)
{
if (empty($attrib)) {
return $this->attribs;
}
if (!isset($this->attribs[$attrib])) {
throw new \Exception("Undefined_Attribute");
}
return $this->attribs[$attrib];
}
/**
* get entity's attributes
*
* @param mixed $attribute
* @return string|array
* @throws \Exception
*/
public function get ($attribute = null)
{
try
{
if (is_array($attribute))
{
$arr = [];
foreach ($attribute as $key) {
$arr[] = $this->attribute($key);
}
return $arr;
}
return $this->attribute($attribute);
}
catch (\Exception $e)
{
if ($e->getMessage() == "Undefined_Attribute" )
{
return $this->attribute();
}
}
}
/**
* get entity's properties
* properties = $this->attribs[$name] - this $name is a property
*
* @return void
*/
public function getAttribKeys ($prop = null)
{
if (isset($prop))
{
if (!isset($this->props[$prop]))
{
throw new \Exception("Undefined_Prop");
}
return $this->props[$prop];
}
return $this->props;
}
/**
* set entity's attribute
*
* @param string $name
* @param array|object $attrib
* @return void
*/
public function set ($name,$attrib)
{
$this->props[] = $name;
$this->attribs[$name] = $attrib;
}
}
?>
AttributeMapContainer Class
<?php
/**
* © Maleesha Gimshan - 2021 - github.com/maleeshagimshan98
*
* Entity's Attribute Container Class
*/
namespace Infinite\DataMapper\Entity;
/**
* Attribute Map Container
*/
class AttributeMapContainer
{
/**
* entity attributes
*
* @var array
*/
protected $attrib_map = [];
/**
* 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 attributes
*
* @param mixed $attribute
* @return string|array
* @throws \Exception
*/
public function get ($attribute = null)
{
if (empty($attribute))
{
$arr = [];
foreach ($this->attrib_map as $key) {
$arr[] = $key;
}
return $arr;
}
if (is_array($attribute))
{
$arr = [];
foreach ($attribute as $key) {
$arr[] = $this->map($key);
}
return $arr;
}
return $this->map($attribute); /** if $attribute === String */
}
public function set ($name,$attrib)
{
$this->attrib_map[$name] = $attrib;
}
}
?>
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\AttributeContainer;
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;
/**
* attributes mapped to column names - table_name.column_name
*
* @var array
*/
public $attribs_map;
/**
* 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 AttributeContainer();
$this->attribs_map = new AttributeMapContainer($name);
$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->primary;
}
/**
* set primary key
*
* @param object $attrib
* @return void
*/
public function setPrimaryKey ($attrib,$name)
{
if (isset($attrib->primary) && $attrib->primary)
{
$this->primary[] = $attrib->name ?? $name ;
}
}
//....
/**
* map entity attributes from configuration
*
* @param object $attribs entity attributes
* @param string $name entity table name
* @return Infinite\DataMapper\Entity\AttributeContainer attribute container
*/
protected function init ($attribs,$name) : AttributeContainer
{ /* TESTING */ //echo \json_encode($attribs);
$keys = [];
foreach ($attribs as $key => $value) {
if ($this->entity_table == "")
{
$this->entity_table = $name;
}
$this->setAttribute($key,$value);
$this->setPrimaryKey($key,$value);
if ($key === "_assoc") {
$this->associations->set($value);
}
}
$this->initEntityResult();
return $this->attribs;
}
/**
* initialize EntityResult object
*
* @return void
*/
protected function initEntityResult ()
{
$keys = $this->attribs->getAttribKeys();
$keys = array_merge($keys,$this->associations->getAssociationKeys());
$this->entityResult = new EntityResult($keys,$this->entityName);
}
/**
* get entity result object
*
* @return Infinite\DataMapper\Entity\EntityResult object
*/
public function getEntityResult ()
{
return $this->entityResult;
}
/**
* set entity's attributes
*
* @param string $name
* @param string|object $attrib
* @return void
*/
protected function setAttribute ($name,$attrib)
{//echo \json_encode($attrib);
$attributeName = $this->parseAttributeName($name,$attrib);
if ($attributeName)
{
//setting the attribute mapping to columns
$this->attribs_map->set($name,$attributeName);
$this->attribs->set($name,$attrib);
}
}
/**
* 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 string
*/
protected function parseAttributeName ($attribName,$attrib)
{
if (\is_object($attrib))
{
if (!empty($attrib->name))
{
$attribute = $attrib->name;
}
elseif (!empty($attrib->primary))
{ //attribs with primary key, but no name comes here
$attribute = $attribName;
}
else
{
return false; //TODO - CHECK -- associations comes here
}
}
elseif (is_string($attrib))
{
$attribute = $attrib;
}
return $attribute;
}
/**
* 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_map->get($attributeName));
}
$attribs = $this->attribs->get($attributeName);
if (is_array($attribs))
{
$columns = [];
foreach ($attribs as $key => $val)
{
$columns[] = $this->withTable($this->attribs_map->get($val));
}
return $columns;
}
return $this->withTable($this->attribs_map->get($attributeName));
}
/**
* 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 = [];
$attribs = $this->attribs->get();
foreach ($attribs as $key => $value)
{
$attributeName = $this->parseAttributeName($key,$value);
$arr[$key] = $data->$attributeName;
}
return new EntityResult($arr,$this->entityName); //always return a new instance
}
/**
* process entity result object for insertion, based on entity's atrributes
* (only entity owned attributes)
*
* @param [type] $data
* @return void
*/
public function insertProps ($data)
{
$arr = [];
$attribs = $this->attribs->getAttribKeys();
foreach ($attribs as $key)
{
$arr[] = $data[$key];
}
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__)."/vendor/autoload.php";
// Entity classes extended from Entity base class
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\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 querying 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
* @return void
*/
protected function setCurrentEntity ($entity)
{
$this->currentEntity = $this->entities->get($entity);
}
/**
* 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 void
*/
protected function hydrate ($res)
{
$currentEntityName = $this->currentEntity->name();
$entityCollection = [];
foreach ($res as $key)
{
$result = $this->entities->entity($currentEntityName)->hydrate($key);
if (!$this->currentAssociated->is_empty())
{
$result = $this->hydrateAssociated($result,$key);
}
$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 $entity entiy object
* @param array|object $data data fetched from database
* @return Infinite\DataMapper\Entity
*/
protected function hydrateAssociated ($entity,$data)
{
foreach ($this->currentAssociated->getAll() as $key => $value)
{
$entity->set($key,$this->entities->entity($key)->hydrate($data));
}
return $entity;
}
/**
* 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
]);
/* TESTING */// echo $sql;
/* TESTING */ //echo json_encode($res);
return $this->hydrate($res);
//return $this->result($res);
}
if ($this->action === "insert")
{
$isDone = true; //CHECK
foreach ($this->sqlStatementStack as $sql)
{
$this->queryDb->transaction();
$isDone = $this->queryDb->insert((object)[
"sql" => $sql[0],
"data" => $sql[1]
]);
}
if ($isDone)
{
$this->queryDb->commit();
}
else {
$this->queryDb->rollback();
}
}
}
/**
* save an entity to database
*
* @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)
{
$this->action = "insert";
$this->setCurrentEntity($entity->name());
$entity->setFromArray($values);
$sqlStatement[] = $this->queryBuilder->insert(
$this->currentEntity->table(),
$this->currentEntity->attribs_map->get(),
$this->currentEntity->insertProps($values)
)->getSqlString();
$sqlStatement[] = $this->currentEntity->insertProps($values);
$this->sqlStatementStack[] = $sqlStatement;
//ONLY MAIN ENTITY IS BEIGN INSERTED
}
/**
* execute raw sql statements directly
* instead of using this ORM
*
* @param string $sql sql query string
* @param object $data data to be bound on the prepared query string
* @return mixed
*/
public function rawSql ($sql,$data)
{
}
}
?>
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?
composer install
or update is triggering error ` [RuntimeException] Could not scan for classes inside "EntityManager/" which does not appear to be a file nor a folder ` \$\endgroup\$