chrisandchris/passive-record-orm

View on GitHub
src/ChrisAndChris/Common/RowMapperBundle/Services/Mapper/RowMapper.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace ChrisAndChris\Common\RowMapperBundle\Services\Mapper;

use ChrisAndChris\Common\RowMapperBundle\Entity\EmptyEntity;
use ChrisAndChris\Common\RowMapperBundle\Entity\Entity;
use ChrisAndChris\Common\RowMapperBundle\Entity\PopulateEntity;
use ChrisAndChris\Common\RowMapperBundle\Entity\StrictEntity;
use ChrisAndChris\Common\RowMapperBundle\Entity\WeakEntity;
use ChrisAndChris\Common\RowMapperBundle\Events\Mapping\PopulationEvent;
use ChrisAndChris\Common\RowMapperBundle\Events\MappingEvents;
use ChrisAndChris\Common\RowMapperBundle\Exceptions\DatabaseException;
use ChrisAndChris\Common\RowMapperBundle\Exceptions\InvalidOptionException;
use ChrisAndChris\Common\RowMapperBundle\Exceptions\Mapping\InsufficientPopulationException;
use ChrisAndChris\Common\RowMapperBundle\Services\Mapper\Encryption\EncryptionServiceInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * @name RowMapper
 * @version    2.0.1
 * @since      v1.0.0
 * @package    RowMapperBundle
 * @author     ChrisAndChris
 * @link       https://github.com/chrisandchris
 */
class RowMapper
{

    /** @var \ChrisAndChris\Common\RowMapperBundle\Services\Mapper\TypeCaster */
    public $typeCaster;
    /**
     * @var EncryptionServiceInterface[]
     */
    private $encryptionServices = [];
    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * RowMapper constructor.
     *
     * @param EventDispatcherInterface                                         $eventDispatcher
     * @param \ChrisAndChris\Common\RowMapperBundle\Services\Mapper\TypeCaster $typeCaster
     */
    public function __construct(
        EventDispatcherInterface $eventDispatcher,
        TypeCaster $typeCaster
    ) {
        $this->eventDispatcher = $eventDispatcher;
        $this->typeCaster = $typeCaster;
    }

    /**
     * Add a new encryption ability
     *
     * @param EncryptionServiceInterface $encryptionService
     */
    public function addEncryptionAbility(
        EncryptionServiceInterface $encryptionService
    ) {
        $this->encryptionServices[] = $encryptionService;
    }

    /**
     * Map a single result from a statement
     *
     * @param \PDOStatement $statement the statement to map
     * @param Entity        $entity    the entity to map into
     * @return Entity
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
     */
    public function mapSingleFromResult(
        \PDOStatement $statement,
        Entity $entity
    ) {
        $list = $this->mapFromResult($statement, $entity, 1);
        if (count($list) == 0) {
            throw new NotFoundHttpException;
        }

        return $list[0];
    }

    /**
     * Maps a result from a statement into an entity
     *
     * @param \PDOStatement $statement   the statement to map
     * @param Entity        $entity      the entity to map to
     * @param int           $limit       max amount of rows to map
     * @param array         $mappingInfo mapping information (typecaset e.g.)
     * @return Entity[] list of entities
     */
    public function mapFromResult(
        \PDOStatement $statement,
        Entity $entity = null,
        $limit = null,
        array $mappingInfo = []
    ) {
        $return = [];
        $count = 0;
        while (false !== ($row = $statement->fetch(\PDO::FETCH_ASSOC)) &&
            (++$count <= $limit || $limit === null)) {
            if ($entity === null) {
                $return[] = $this->mapRow($row, null, $mappingInfo);
            } else {
                $return[] = $this->mapRow($row, clone $entity, $mappingInfo);
            }
        }

        return $return;
    }

    /**
     * Map a single row by calling setter if possible or
     * accessing the properties directly if no setter available<br />
     * <br />
     * The setter name is generated by a key of $row, by following rules:<br />
     * <ul>
     *  <li>underscores are removed, next letter is uppercase</li>
     *  <li>first letter goes uppercase</li>
     *  <li>a "set" string is added to the beginning</li>
     * </ul>
     *
     * @param array $row         the row to map
     * @param       $entity      entity to map to
     * @param array $mappingInfo mapping info (typecast e.g.)
     * @return array|Entity $entity the mapped entity
     * @throws InvalidOptionException if EmptyEntity is given
     */
    public function mapRow(
        array $row,
        Entity $entity = null,
        array $mappingInfo = []
    ) {
        if ($entity instanceof EmptyEntity) {
            throw new InvalidOptionException(
                'You are not allowed to map rows to an EmptyEntity instance'
            );
        }
        if (!isset($entity)) {
            $entity = new EmptyEntity();
        }
        $this->populateFields($row, $entity, $mappingInfo);

        $entity = $this->checkForDecryption($entity);

        return $entity;
    }

    /**
     * @param array  $row                      the row to map
     * @param Entity $entity                   entity to map to
     * @param array  $mappingInfo              mapping info (typecast e.g.)
     * @throws InsufficientPopulationException if strict entity is not fully
     *                                         populated
     */
    public function populateFields(
        array $row,
        Entity $entity,
        array $mappingInfo
    ) {
        $entityFiller = $this->getEntityFiller();

        $count = 0;
        foreach ($row as $field => $value) {
            $count += $entityFiller($entity, $field, $value, $mappingInfo);
        }

        if ($entity instanceof PopulateEntity) {
            $event = $this->eventDispatcher->dispatch(
                MappingEvents::POST_MAPPING_ROW_POPULATION,
                new PopulationEvent($entity, $entityFiller)
            );
            $count += $event->getWrittenFieldCount();
        }

        if ($entity instanceof StrictEntity && count($row) != $count) {
            throw new InsufficientPopulationException(
                sprintf(
                    'Requires entity "%s" to get populated for %d fields, but did only %d',
                    get_class($entity),
                    count($row),
                    $count
                )
            );
        }
    }

    /**
     * @return \Closure
     */
    private function getEntityFiller() : \Closure
    {
        return
            function (
                Entity &$entity,
                $field,
                $value,
                $mappingInfo = [],
                $weak = false
            ) {
                $methodName = $this->buildMethodName($field);

                // cast if necessary
                if (isset($mappingInfo[$field]) &&
                    ($value !== null || $mappingInfo[$field] === 'bool')) {
                    $value = $this->getTypeCaster()
                                  ->cast($mappingInfo[$field], $value);
                }

                if ($weak) {
                    // dash-case to camelCase when we're using weak mode
                    $field = str_replace('_', '', ucwords($field, '_'));
                    $field = lcfirst($field);
                }

                if (method_exists($entity, $methodName)) {
                    // set using method set{property}
                    $entity->$methodName($value);

                    return 1;
                } elseif (property_exists($entity, $field) ||
                    $entity instanceof EmptyEntity
                ) {
                    // set direct to property
                    $entity->$field = $value;

                    return 1;
                } elseif (!($entity instanceof WeakEntity) && !$weak) {
                    throw new DatabaseException(
                        sprintf('No property %s found for Entity',
                            $field)
                    );
                }

                return 0;
            };
    }

    /**
     * Build a method name
     *
     * @param $key
     * @return string
     */
    public function buildMethodName($key)
    {
        $partials = explode('_', $key);
        foreach ($partials as $idx => $part) {
            $partials[$idx] = ucfirst($part);
        }

        return 'set' . implode('', $partials);
    }

    private function getTypeCaster()
    {
        return $this->typeCaster;
    }

    /**
     * @param Entity $entity
     * @return array|EmptyEntity|Entity
     */
    private function checkForDecryption(Entity $entity)
    {
        $entity = $this->runDecryption($entity);

        if ($entity instanceof EmptyEntity) {
            $fields = [];
            foreach (get_object_vars($entity) as $property => $value) {
                $fields[$property] = $value;
            }
            $entity = $fields;

            return $entity;
        }

        return $entity;
    }

    /**
     * Run the decryption process
     *
     * @param Entity $entity
     * @return Entity
     */
    private function runDecryption(Entity $entity)
    {
        foreach ($this->encryptionServices as $encryptionService) {
            if ($encryptionService->isResponsible($entity)) {
                return $encryptionService->decrypt($entity);
            }
        }

        return $entity;
    }

    /**
     * Maps a statement to an associative array<br />
     * <br />
     * The closure is used to map any row, it must give back an array.<br />
     * The array <i>may</i> contain an index "key" with the desired key value
     * of the returned array and it <i>must</i> contain an index "value" with
     * the value to map
     *
     *
     * @param \PDOStatement $statement the statement to map
     * @param Entity        $entity    the entity to map from
     * @param \Closure      $callable  the callable to use to map any row
     * @return array
     * @throws InvalidOptionException if invalid input is given
     */
    public function mapToArray($statement, Entity $entity, \Closure $callable)
    {
        $array = $this->mapFromResult($statement, $entity);
        $return = [];
        foreach ($array as $row) {
            $a = $callable($row);
            if (!is_array($a)) {
                throw new InvalidOptionException('Callable must return array with at least index "value"');
            }
            if (isset($a['key']) && !empty($a['key'])) {
                $return[$a['key']] = $a['value'];
            } else {
                $return[] = $a['value'];
            }
        }

        return $return;
    }
}