lib/Mapper.php
<?php
namespace Freckle;
use Doctrine\DBAL\Types\Type;
class Mapper
{
use Partial\Camelize;
/** @var Connection */
protected $connection;
/** @var Mapping */
protected $mapping;
/** @var array */
protected $identityMap;
/**
* @param Connection $connection
* @param Mapping $mapping
*/
public function __construct(Connection $connection, Mapping $mapping)
{
$this->connection = $connection;
$this->mapping = $mapping;
}
/**
* @param array $data
* @return Entity
*/
public function entity(array $data = [])
{
/** @var Entity $entity */
$entityClass = $this->mapping->entityClass();
$entity = new $entityClass;
foreach ($this->mapping->fields() as $field => $definition) {
if (isset($data[$field]) || !isset($definition['default'])) {
continue;
}
$data[$field] = is_callable($definition['default']) ? $definition['default']() : $definition['default'];
}
foreach ($this->mapping->relations() as $relation => $definition) {
$data[$relation] = function (Entity $entity) use ($definition) {
return $this->relation($entity, $definition);
};
}
$this->bind($entity, $data);
$entity->flag(Entity::FLAG_NEW);
return $entity;
}
/**
* @param array $data
* @return Entity
*/
public function create(array $data)
{
$entity = $this->entity($data);
$this->insert($entity);
return $entity;
}
/**
* @param Entity $entity
*/
public function insert(Entity $entity)
{
$data = $this->flatten($entity);
$this->validate($data);
$this->connection->insert($this->mapping->table(), $data);
$entity->unflag(Entity::FLAG_NEW | Entity::FLAG_DIRTY);
$sequence = $this->mapping->sequence();
if ($sequence) {
$value = isset($data[$sequence['field']]) ? $data[$sequence['field']] : $this->connection->lastInsertId($sequence['name']);
$this->bind($entity, [$sequence['field'] => $value]);
}
$this->identityMap[$this->key($entity)] = $entity;
}
/**
* @param Entity $entity
*/
public function update(Entity $entity)
{
if (!$entity->flagged(Entity::FLAG_DIRTY)) {
return;
}
$data = $this->flatten($entity);
$this->validate($data);
$identifier = [];
foreach (array_keys(array_intersect_key($data, $this->mapping->primary())) as $field) {
$identifier[$field] = $data[$field];
unset($data[$field]);
}
$this->connection->update($this->mapping->table(), $data, $identifier);
}
/**
* @param Entity $entity
*/
public function save(Entity $entity)
{
$this->{$entity->flagged(Entity::FLAG_NEW) ? 'insert' : 'update'}($entity);
}
/**
* @param array $data
* @param array $conditions
* @return Entity
*/
public function upsert(array $data = [], array $conditions = [])
{
$entity = $this->first($conditions) ? : $this->entity();
$this->bind($entity, $data);
$this->save($entity);
return $entity;
}
/**
* @param Entity $entity
*/
public function delete(Entity $entity)
{
$data = $this->flatten($entity);
$identifier = [];
$sequence = null;
foreach (array_intersect_key($data, $this->mapping->primary()) as $field => $value) {
$identifier[$field] = $value;
}
$this->connection->delete($this->mapping->table(), $identifier);
$entity->flag(Entity::FLAG_NEW);
unset($this->identityMap[$this->key($entity)]);
$sequence = $this->mapping->sequence();
if ($sequence) {
$this->bind($entity, [$sequence['field'] => null]);
}
}
/**
* @param array $conditions
* @return Query
*/
public function find(array $conditions = [])
{
return $this->connection->select($this->mapping->table(), $conditions)->mapper($this);
}
/**
* @param array $conditions
* @return Entity|null
*/
public function first(array $conditions = [])
{
$key = $this->key($conditions);
if (isset($this->identityMap[$key])) {
return $this->identityMap[$key];
}
$result = $this->find($conditions)->limit(1);
return isset($result[0]) ? $result[0] : null;
}
/**
* @param array $results
* @return array
*/
public function collection(array $results)
{
$collection = [];
foreach ($results as $result) {
$key = $this->key($result);
if (!isset($this->identityMap[$key])) {
$entity = $this->entity();
$this->expand($entity, $result);
$entity->unflag(Entity::FLAG_NEW);
$this->identityMap[$key] = $entity;
}
$collection[] = $this->identityMap[$key];
}
return $collection;
}
/**
* @param Entity $entity
* @param array $data
*/
protected function expand(Entity $entity, array $data)
{
$result = [];
$fields = $this->mapping->fields();
$data = array_intersect_key($data, $fields);
$platform = $this->connection->getDatabasePlatform();
foreach ($data as $field => $value) {
$result[$field] = Type::getType($fields[$field][0])->convertToPHPValue($value, $platform);
}
$this->bind($entity, $result);
}
/**
* @param Entity $entity
* @return array
*/
protected function flatten(Entity $entity)
{
$result = [];
$fields = $this->mapping->fields();
$data = array_intersect_key($entity->data(), $fields);
$platform = $this->connection->getDatabasePlatform();
foreach ($data as $field => $value) {
$result[$field] = Type::getType($fields[$field][0])->convertToDatabaseValue($value, $platform);
}
return $result;
}
/**
* @param Entity $entity
* @param array $data
*/
protected function bind(Entity $entity, array $data)
{
foreach ($data as $field => $value) {
$entity->{'set' . $this->camelize($field)}($value);
}
}
/**
* @param array $data
* @throws Exception\ValidationException
*/
protected function validate(array $data)
{
foreach ($this->mapping->required() as $field) {
if (!isset($data[$field])) {
throw new Exception\ValidationException('Missing required field ' . $field . ' for ' . $this->mapping->entityClass());
}
}
}
/**
* @param Entity $entity
* @param $definition
* @return Query
*/
protected function relation(Entity $entity, $definition)
{
list($type, $entityClass, $conditions) = $definition;
if (!is_subclass_of($entityClass, Entity::class)) {
throw new \InvalidArgumentException('Related $entityClass must be a subclass of ' . Entity::class);
}
foreach ($conditions as $key => $value) {
if (!is_string($value) || !preg_match('/^this\.(?P<field>.+)$/', $value, $match)) {
continue;
}
$conditions[$key] = $entity->{'get' . $this->camelize($match['field'])}();
}
if (isset($definition['through'])) {
$values = [];
list($tableThrough, $fieldThrough) = explode('.', $definition['through']);
foreach ($this->connection->select($tableThrough, $conditions) as $data) {
$values[] = $data[$fieldThrough];
}
$conditions = [isset($definition['field']) ? $definition['field'] : 'id' => $values];
}
$method = str_replace(['one', 'many'], ['first', 'find'], $type);
return $this->connection->mapper($entityClass)->$method($conditions);
}
/**
* @param Entity|array $data
* @return string
*/
protected function key($data)
{
if ($data instanceof Entity) {
$data = $data->data();
}
ksort($data);
$data = array_intersect_key($data, $this->mapping->primary());
return http_build_query($data);
}
}