mathieupetrini/doctrinedatatable

View on GitHub
src/DoctrineDatatable/Datatable.php

Summary

Maintainability
A
25 mins
Test Coverage
<?php

namespace DoctrineDatatable;

use Doctrine\ORM\QueryBuilder;
use DoctrineDatatable\Exception\GlobalFilterWithHavingColumn;
use DoctrineDatatable\Exception\MinimumColumn;

/**
 * Class Datatable.
 *
 * @author Mathieu Petrini <mathieupetrini@gmail.com>
 */
class Datatable
{
    /**
     * @var string
     */
    private $identifier;

    /**
     * @var int
     */
    private $resultPerPage;

    /**
     * @var string
     */
    private $nameIdentifier;

    /**
     * @var bool
     */
    private $globalSearch;

    /**
     * @var QueryBuilder
     */
    protected $query;

    /**
     * @var QueryBuilder
     */
    protected $final_query;

    /**
     * @var Column[]
     */
    protected $columns;

    private const DEFAULT_NAME_IDENTIFIER = 'DT_RowId';

    public const RESULT_PER_PAGE = 30;

    /**
     * Datatable constructor.
     *
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param QueryBuilder $query
     * @param string       $identifier
     * @param Column[]     $columns
     * @param int|null     $resultPerPage
     *
     * @throws MinimumColumn
     */
    public function __construct(
        QueryBuilder $query,
        string $identifier,
        array $columns,
        ?int $resultPerPage = self::RESULT_PER_PAGE
    ) {
        if (empty($columns)) {
            throw new MinimumColumn();
        }
        $this->query = $query;
        $this->identifier = $identifier;
        $this->columns = $columns;
        $this->resultPerPage = $resultPerPage ?? self::RESULT_PER_PAGE;
        $this->nameIdentifier = self::DEFAULT_NAME_IDENTIFIER;
        $this->globalSearch = false;
    }

    /**
     * PRIVATE METHODS.
     */

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param mixed[] $filters
     *
     * @return mixed[]
     */
    private function createGlobalFilters(array $filters): array
    {
        $temp = array(
            'columns' => array(),
        );
        array_map(function () use ($filters, &$temp) {
            $temp['columns'][] = array(
                'search' => array(
                    'value' => $filters['search'][Column::GLOBAL_ALIAS],
                ),
            );
        }, $this->columns);

        return $temp;
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param mixed[] $filters
     *
     * @return string[]
     *
     * @throws Exception\ResolveColumnNotHandle
     * @throws Exception\UnfilterableColumn
     * @throws Exception\WhereColumnNotHandle
     */
    private function createCondition(array $filters): array
    {
        $where = '';
        $having = '';

        foreach ($filters['columns'] as $index => $filter) {
            if (isset($this->columns[$index]) && '' !== $filter['search']['value']) {
                $temp = '('.$this->columns[$index]->where($this->final_query, $filter['search']['value']).')';

                $this->columns[$index]->isHaving() ?
                    $having .= (!empty($having) ? ' AND ' : '').$temp :
                    $where .= (!empty($where) ? ' '.($this->globalSearch ? 'OR' : 'AND').' ' : '').$temp;
            }
        }

        return array(
            'where' => $where,
            'having' => $having,
        );
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param bool $withAlias (optional) (default=true)
     *
     * @return string
     */
    private function processColumnIdentifier(bool $withAlias = true): string
    {
        return $this->final_query->getRootAliases()[0].'.'.$this->identifier.($withAlias ? ' AS '.$this->nameIdentifier : '');
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param Column $column
     */
    private function processColumnSelect(Column $column): void
    {
        $this->final_query->addSelect($column->getName().' AS '.$column->getAlias());
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param int    $index
     * @param string $direction
     *
     * @return Datatable
     */
    private function orderBy(int $index, string $direction): self
    {
        $this->final_query->orderBy(
            \array_slice($this->columns, $index, 1)[0]->getAliasOrderBy(),
            $direction
        );

        return $this;
    }

    /**
     * @param mixed[] $filters
     *
     * @return Datatable
     */
    private function limit(array $filters): self
    {
        $this->final_query->setFirstResult($filters['start'] ?? 0)
            ->setMaxResults($filters['length'] ?? $this->resultPerPage);

        return $this;
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @return int
     */
    private function count(): int
    {
        $query = clone $this->final_query;
        $result = $query->select('COUNT(DISTINCT '.$this->processColumnIdentifier(false).') as count')
            ->resetDQLPart('groupBy')
            ->resetDQLPart('orderBy')
            ->setFirstResult(0)
            ->setMaxResults(null)
            ->getQuery()
            ->getScalarResult();

        return !empty($result) ?
            (int) $result[0]['count'] :
            0;
    }

    /**
     * PROTECTED METHODS.
     */

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param mixed[] $filters
     *
     * @return QueryBuilder
     *
     * @throws Exception\ResolveColumnNotHandle
     * @throws Exception\UnfilterableColumn
     * @throws Exception\WhereColumnNotHandle
     */
    protected function createFoundationQuery(array $filters): QueryBuilder
    {
        // If global search we erase all specific where and only keep the unified filter
        if ($this->globalSearch && isset($filters['search']) && !empty($filters['search'][Column::GLOBAL_ALIAS])) {
            $filters = $this->createGlobalFilters($filters);
        }

        $conditions = isset($filters['columns']) ?
            $this->createCondition($filters) :
            array();

        if (isset($conditions['where']) && !empty($conditions['where'])) {
            $this->final_query->andWhere($conditions['where']);
        }

        if (isset($conditions['having']) && !empty($conditions['having'])) {
            $this->final_query->andHaving($conditions['having']);
        }

        return $this->final_query;
    }

    /**
     * @return QueryBuilder
     */
    protected function createQueryResult(): QueryBuilder
    {
        $this->final_query = clone $this->query;
        $this->final_query->select($this->processColumnIdentifier());
        foreach ($this->columns as $column) {
            $this->processColumnSelect($column);
        }

        return $this->final_query;
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param mixed[] $filters
     *
     * @return mixed[]
     */
    protected function data(array $filters): array
    {
        $this->limit($filters);

        return $this->final_query
            ->getQuery()
            ->getResult();
    }

    /**
     * PUBLIC METHODS.
     */

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param mixed[] $filters
     *
     * @return QueryBuilder
     *
     * @throws Exception\ResolveColumnNotHandle
     * @throws Exception\UnfilterableColumn
     * @throws Exception\WhereColumnNotHandle
     */
    public function createFinalQuery(array $filters): QueryBuilder
    {
        $this->createQueryResult();
        if (isset($filters['order'])) {
            $this->orderBy(
                $filters['order'][0]['column'],
                $filters['order'][0]['dir']
            );   
        }

        return $this->createFoundationQuery($filters);
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @param mixed[] $filters
     *
     * @return mixed[]
     *
     * @throws Exception\ResolveColumnNotHandle
     * @throws Exception\UnfilterableColumn
     * @throws Exception\WhereColumnNotHandle
     */
    public function get(array $filters): array
    {
        $this->createFinalQuery($filters);
        $data = $this->data($filters);

        return array(
            'recordsTotal' => $this->count(),
            'recordsFiltered' => \count($data),
            'data' => $data,
        );
    }

    /**
     * @author Mathieu Petrini <mathieupetrini@gmail.com>
     *
     * @return string
     */
    public function export(): string
    {
        return stream_get_contents(
            (new Export())
            ->setDatatable($this)
            ->export()
        );
    }

    /**
     * GETTERS / SETTERS.
     */

    /**
     * @return string
     */
    public function getNameIdentifier(): string
    {
        return $this->nameIdentifier;
    }

    /**
     * @param string $nameIdentifier
     *
     * @return Datatable
     */
    public function setNameIdentifier(string $nameIdentifier): self
    {
        $this->nameIdentifier = $nameIdentifier;

        return $this;
    }

    /**
     * @param bool $globalSearch
     *
     * @return Datatable
     *
     * @throws GlobalFilterWithHavingColumn
     */
    public function setGlobalSearch(bool $globalSearch): self
    {
        $this->globalSearch = $globalSearch;
        if ($this->globalSearch) {
            foreach ($this->columns as $column) {
                if ($column->isHaving()) {
                    throw new GlobalFilterWithHavingColumn();
                }
            }
        }

        return $this;
    }
}