<?php

/*
 * This file is part of the DataGridBundle.
 *
 * (c) Abhoryo <abhoryo@free.fr>
 * (c) Stanislav Turza
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace APY\DataGridBundle\Grid\Source;

use APY\DataGridBundle\Grid\Column\Column;
use APY\DataGridBundle\Grid\Exception\PropertyAccessDeniedException;
use APY\DataGridBundle\Grid\Helper\ColumnsIterator;
use APY\DataGridBundle\Grid\Mapping\Driver\DriverInterface;
use APY\DataGridBundle\Grid\Row;
use APY\DataGridBundle\Grid\Rows;

abstract class Source implements DriverInterface
{
    protected $prepareQueryCallback = null;
    protected $prepareRowCallback = null;
    protected $data = null;
    protected $items = [];
    protected $count;

    /**
     * @param \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder
     */
    public function prepareQuery($queryBuilder)
    {
        if (is_callable($this->prepareQueryCallback)) {
            call_user_func($this->prepareQueryCallback, $queryBuilder);
        }
    }

    /**
     * @param \APY\DataGridBundle\Grid\Row $row
     *
     * @return \APY\DataGridBundle\Grid\Row|null
     */
    public function prepareRow($row)
    {
        if (is_callable($this->prepareRowCallback)) {
            return call_user_func($this->prepareRowCallback, $row);
        }

        return $row;
    }

    /**
     * @param callable $callback
     *
     * @return $this
     */
    public function manipulateQuery($callback = null)
    {
        $this->prepareQueryCallback = $callback;

        return $this;
    }

    /**
     * @param \Closure $callback
     */
    public function manipulateRow(\Closure $callback = null)
    {
        $this->prepareRowCallback = $callback;

        return $this;
    }

    /**
     * Find data for current page.
     *
     * @abstract
     *
     * @param ColumnsIterator $columns
     * @param int                                      $page             Page Number
     * @param int                                      $limit            Rows Per Page
     * @param int                                      $gridDataJunction Grid data junction
     *
     * @return \APY\DataGridBundle\Grid\Rows
     */
    // @todo: typehint?
    abstract public function execute($columns, $page = 0, $limit = 0, $maxResults = null, $gridDataJunction = Column::DATA_CONJUNCTION);

    /**
     * Get Total count of data items.
     *
     * @param int $maxResults
     *
     * @return int
     */
    abstract public function getTotalCount($maxResults = null);

    /**
     * Set container.
     *
     * @abstract
     *
     * @param  $container
     */
    abstract public function initialise($container);

    /**
     * @abstract
     *
     * @param $columns
     */
    abstract public function getColumns($columns);

    public function getClassColumns($class, $group = 'default')
    {
        return [];
    }

    public function getFieldsMetadata($class, $group = 'default')
    {
        return [];
    }

    public function getGroupBy($class, $group = 'default')
    {
        return [];
    }

    abstract public function populateSelectFilters($columns, $loop = false);

    /**
     * Return source hash string.
     *
     * @abstract
     */
    abstract public function getHash();

    /**
     * Delete one or more objects.
     *
     * @abstract
     *
     * @param array $ids
     */
    abstract public function delete(array $ids);

    /**
     * Use data instead of fetching the source.
     *
     * @param array|object $data
     */
    public function setData($data)
    {
        $this->data = $data;

        return $this;
    }

    /**
     * Get the loaded data.
     *
     * @return array|object
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * Check if data is loaded.
     *
     * @return bool
     */
    public function isDataLoaded()
    {
        return $this->data !== null;
    }

    /**
     * Gets an array of data items for rows from the set data.
     *
     * @return array
     */
    protected function getItemsFromData($columns)
    {
        $items = [];

        foreach ($this->data as $key => $item) {
            foreach ($columns as $column) {
                $fieldName = $column->getField();
                $fieldValue = '';

                if ($this instanceof Entity) {
                    // Mapped field
                    $itemEntity = $item;
                    if (strpos($fieldName, '.') === false) {
                        $functionName = ucfirst($fieldName);
                    } else {
                        // loop through all elements until we find the final entity and the name of the value for which we are looking
                        $elements = explode('.', $fieldName);
                        while ($element = array_shift($elements)) {
                            if (count($elements) > 0) {
                                $itemEntity = call_user_func([$itemEntity, 'get' . $element]);
                            } else {
                                $functionName = ucfirst($element);
                            }
                        }
                    }

                    // Get value of the column
                    if (isset($itemEntity->$fieldName)) {
                        $fieldValue = $itemEntity->$fieldName;
                    } elseif (is_callable([$itemEntity, $fullFunctionName = 'get' . $functionName])
                           || is_callable([$itemEntity, $fullFunctionName = 'has' . $functionName])
                           || is_callable([$itemEntity, $fullFunctionName = 'is' . $functionName])) {
                        $fieldValue = call_user_func([$itemEntity, $fullFunctionName]);
                    } else {
                        throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public or has no accessor.', $fieldName));
                    }
                } elseif (isset($item[$fieldName])) {
                    $fieldValue = $item[$fieldName];
                }

                $items[$key][$fieldName] = $fieldValue;
            }
        }

        return $items;
    }

    /**
     * Find data from array|object.
     *
     * @param Column[] $columns
     * @param int      $page
     * @param int      $limit
     * @param int      $maxResults
     *
     * @return Rows
     */
    public function executeFromData($columns, $page = 0, $limit = 0, $maxResults = null)
    {
        // Populate from data
        $items = $this->getItemsFromData($columns);
        $serializeColumns = [];

        foreach ($this->data as $key => $item) {

            foreach ($columns as $column) {
                $fieldName = $column->getField();
                $fieldValue = $items[$key][$fieldName];
                $dataIsNumeric = ($column->getType() == 'number' || $column->getType() == 'boolean');

                if ($column->getType() === 'array') {
                    $serializeColumns[] = $column->getId();
                }

                // Filter
                if ($column->isFiltered()) {
                    // Some attributes of the column can be changed in this function
                    $filters = $column->getFilters('vector');

                    if ($column->getDataJunction() === Column::DATA_DISJUNCTION) {
                        $disjunction = true;
                        $keep = false;
                    } else {
                        $disjunction = false;
                        $keep = true;
                    }

                    $found = false;
                    foreach ($filters as $filter) {
                        $operator = $filter->getOperator();
                        $value = $filter->getValue();

                        // Normalize value
                        if (!$dataIsNumeric && !($value instanceof \DateTime)) {
                            $value = $this->prepareStringForLikeCompare($value);
                            switch ($operator) {
                                case Column::OPERATOR_EQ:
                                    $value = '/^' . preg_quote($value, '/') . '$/i';
                                    break;
                                case Column::OPERATOR_NEQ:
                                    $value = '/^(?!' . preg_quote($value, '/') . '$).*$/i';
                                    break;
                                case Column::OPERATOR_LIKE:
                                    $value = '/' . preg_quote($value, '/') . '/i';
                                    break;
                                case Column::OPERATOR_NLIKE:
                                    $value = '/^((?!' . preg_quote($value, '/') . ').)*$/i';
                                    break;
                                case Column::OPERATOR_LLIKE:
                                    $value = '/' . preg_quote($value, '/') . '$/i';
                                    break;
                                case Column::OPERATOR_RLIKE:
                                    $value = '/^' . preg_quote($value, '/') . '/i';
                                    break;
                                case Column::OPERATOR_SLIKE:
                                    $value = '/' . preg_quote($value, '/') . '/';
                                    break;
                                case Column::OPERATOR_NSLIKE:
                                    $value = '/^((?!' . preg_quote($value, '/') . ').)*$/';
                                    break;
                                case Column::OPERATOR_LSLIKE:
                                    $value = '/' . preg_quote($value, '/') . '$/';
                                    break;
                                case Column::OPERATOR_RSLIKE:
                                    $value = '/^' . preg_quote($value, '/') . '/';
                                    break;
                            }
                        }

                        // Test
                        switch ($operator) {
                            case Column::OPERATOR_EQ:
                                if ($dataIsNumeric) {
                                    $found = abs($fieldValue - $value) < 0.00001;
                                    break;
                                }
                            case Column::OPERATOR_NEQ:
                                if ($dataIsNumeric) {
                                    $found = abs($fieldValue - $value) > 0.00001;
                                    break;
                                }
                            case Column::OPERATOR_LIKE:
                            case Column::OPERATOR_NLIKE:
                            case Column::OPERATOR_LLIKE:
                            case Column::OPERATOR_RLIKE:
                            case Column::OPERATOR_SLIKE:
                            case Column::OPERATOR_NSLIKE:
                            case Column::OPERATOR_LSLIKE:
                            case Column::OPERATOR_RSLIKE:
                                $fieldValue = $this->prepareStringForLikeCompare($fieldValue, $column->getType());

                                $found = preg_match($value, $fieldValue);
                                break;
                            case Column::OPERATOR_GT:
                                $found = $fieldValue > $value;
                                break;
                            case Column::OPERATOR_GTE:
                                $found = $fieldValue >= $value;
                                break;
                            case Column::OPERATOR_LT:
                                $found = $fieldValue < $value;
                                break;
                            case Column::OPERATOR_LTE:
                                $found = $fieldValue <= $value;
                                break;
                            case Column::OPERATOR_ISNULL:
                                $found = $fieldValue === null;
                                break;
                            case Column::OPERATOR_ISNOTNULL:
                                $found = $fieldValue !== null;
                                break;
                        }

                        // AND
                        if (!$found && !$disjunction) {
                            $keep = false;
                            break;
                        }

                        // OR
                        if ($found && $disjunction) {
                            $keep = true;
                            break;
                        }
                    }

                    if (!$keep) {
                        unset($items[$key]);
                        break;
                    }
                }
            }
        }

        // Order
        foreach ($columns as $column) {
            if ($column->isSorted()) {
                $sortType = SORT_REGULAR;
                $sortedItems = [];
                foreach ($items as $key => $item) {
                    $value = $item[$column->getField()];

                    // Format values for sorting and define the type of sort
                    switch ($column->getType()) {
                        case 'text':
                            $sortedItems[$key] = strtolower($value);
                            $sortType = SORT_STRING;
                            break;
                        case 'datetime':
                        case 'date':
                        case 'time':
                            if ($value instanceof \DateTime) {
                                $sortedItems[$key] = $value->getTimestamp();
                            } else {
                                $sortedItems[$key] = strtotime($value);
                            }
                            $sortType = SORT_NUMERIC;
                            break;
                        case 'boolean':
                            $sortedItems[$key] = $value ? 1 : 0;
                            $sortType = SORT_NUMERIC;
                            break;
                        case 'array':
                            $sortedItems[$key] = json_encode($value);
                            $sortType = SORT_STRING;
                            break;
                        case 'number':
                            $sortedItems[$key] = $value;
                            $sortType = SORT_NUMERIC;
                            break;
                        default:
                            $sortedItems[$key] = $value;
                            $sortType = SORT_REGULAR;
                    }
                }

                if (!empty($sortedItems)) {
                    array_multisort($sortedItems, ($column->getOrder() == 'asc') ? SORT_ASC : SORT_DESC, $sortType, $items);
                }

                break;
            }
        }

        $this->count = count($items);

        // Pagination
        if ($limit > 0) {
            $maxResults = ($maxResults !== null && ($maxResults - $page * $limit < $limit)) ? $maxResults - $page * $limit : $limit;

            $items = array_slice($items, $page * $limit, $maxResults);
        } elseif ($maxResults !== null) {
            $items = array_slice($items, 0, $maxResults);
        }

        $rows = new Rows();
        foreach ($items as $item) {
            $row = new Row();

            if ($this instanceof Vector) {
                $row->setPrimaryField($this->getId());
            }

            foreach ($item as $fieldName => $fieldValue) {
                if ($this instanceof Entity) {
                    if (in_array($fieldName, $serializeColumns)) {
                        if (is_string($fieldValue)) {
                            $fieldValue = unserialize($fieldValue);
                        }
                    }
                }

                $row->setField($fieldName, $fieldValue);
            }

            //call overridden prepareRow or associated closure
            if (($modifiedRow = $this->prepareRow($row)) != null) {
                $rows->addRow($modifiedRow);
            }
        }

        $this->items = $items;

        return $rows;
    }

    public function populateSelectFiltersFromData($columns, $loop = false)
    {
        /* @var $column Column */
        foreach ($columns as $column) {
            $selectFrom = $column->getSelectFrom();

            if ($column->getFilterType() === 'select' && ($selectFrom === 'source' || $selectFrom === 'query')) {

                // For negative operators, show all values
                if ($selectFrom === 'query') {
                    foreach ($column->getFilters('vector') as $filter) {
                        if (in_array($filter->getOperator(), [Column::OPERATOR_NEQ, Column::OPERATOR_NLIKE, Column::OPERATOR_NSLIKE])) {
                            $selectFrom = 'source';
                            break;
                        }
                    }
                }

                // Dynamic from query or not ?
                $item = ($selectFrom === 'source') ? $this->data : $this->items;

                $values = [];
                foreach ($item as $row) {
                    $value = $row[$column->getField()];

                    switch ($column->getType()) {
                        case 'number':
                            $values[$value] = $column->getDisplayedValue($value);
                            break;
                        case 'datetime':
                        case 'date':
                        case 'time':
                            // For document
                            if ($value instanceof \MongoDate || $value instanceof \MongoTimestamp) {
                                $value = $value->sec;
                            }

                            // Mongodb bug ? timestamp value is on the key 'i' instead of the key 't'
                            if (is_array($value) && array_keys($value) == ['t', 'i']) {
                                $value = $value['i'];
                            }

                            $displayedValue = $column->getDisplayedValue($value);
                            $values[$displayedValue] = $displayedValue;
                            break;
                        case 'array':
                            if (is_string($value)) {
                                $value = unserialize($value);
                            }

                            foreach ($value as $val) {
                                $values[$val] = $val;
                            }
                            break;
                        default:
                            $values[$value] = $value;
                    }
                }

                // It avoids to have no result when the other columns are filtered
                if ($selectFrom === 'query' && empty($values) && $loop === false) {
                    $column->setSelectFrom('source');
                    $this->populateSelectFiltersFromData($columns, true);
                } else {
                    if ($column->getType() == 'array') {
                        natcasesort($values);
                    }

                    $values = $this->prepareColumnValues($column, $values);
                    $column->setValues(array_unique($values));
                }
            }
        }
    }

    /**
     * Get Total count of data items.
     *
     * @return int
     */
    public function getTotalCountFromData($maxResults = null)
    {
        return $maxResults === null ? $this->count : min($this->count, $maxResults);
    }

    /**
     * Prepares string to have almost the same behaviour as with a database,
     * removing accents and latin special chars.
     *
     * @param mixed  $inputString
     * @param string $type        for array type, will serialize datas
     *
     * @return string the input, serialized for arrays or without accents for strings
     */
    protected function prepareStringForLikeCompare($input, $type = null)
    {
        if ($type === 'array') {
            $outputString = str_replace(':{i:0;', ':{', serialize($input));
        } else {
            $outputString = $this->removeAccents($input);
        }

        return $outputString;
    }

    private function removeAccents($str)
    {
        $entStr = htmlentities($str, ENT_NOQUOTES, 'UTF-8');
        $noaccentStr = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $entStr);

        return preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $noaccentStr);
    }

    protected function prepareColumnValues(Column $column, $values)
    {
        $existingValues = $column->getValues();
        if (!empty($existingValues)) {
            $intersect = array_intersect_key($existingValues, $values);
            $values = array_replace($values, $intersect);
        }

        return $values;
    }
}


<?php

namespace APY\DataGridBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * This is the class that validates and merges configuration from your app/config files.
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
 */
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('apy_data_grid');

        $rootNode
            ->children()
                ->arrayNode('limits')
                    ->performNoDeepMerging()
                    ->beforeNormalization()
                        ->ifTrue(function ($v) { return !is_array($v); })
                        ->then(function ($v) { return [$v]; })
                    ->end()
                    ->defaultValue([20 => '20', 50 => '50', 100 => '100'])
                    ->prototype('scalar')->end()
                ->end()
                ->booleanNode('persistence')->defaultFalse()->end()
                ->scalarNode('theme')->defaultValue('APYDataGridBundle::blocks.html.twig')->end()
                ->scalarNode('no_data_message')->defaultValue('No data')->end()
                ->scalarNode('no_result_message')->defaultValue('No result')->end()
                ->scalarNode('actions_columns_size')->defaultValue(-1)->end()
                ->scalarNode('actions_columns_title')->defaultValue('Actions')->end()
                ->scalarNode('actions_columns_separator')->defaultValue('<br />')->end() // deprecated
                ->arrayNode('pagerfanta')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->booleanNode('enable')->defaultFalse()->end()
                        ->scalarNode('view_class')->defaultValue('Pagerfanta\View\DefaultView')->end()
                        ->arrayNode('options')
                            ->defaultValue(['prev_message' => '«', 'next_message' => '»'])
                            ->useAttributeAsKey('options')
                            ->prototype('scalar')->end()
                        ->end()
                    ->end()
                ->end()
             ->end();

        return $treeBuilder;
    }
}


<?php

/*
 * This file is part of the DataGridBundle.
 *
 * (c) Abhoryo <abhoryo@free.fr>
 * (c) Stanislav Turza
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace APY\DataGridBundle\Grid;

use Doctrine\ORM\EntityRepository;

class Row
{
    /** @var array */
    protected $fields;

    /** @var string */
    protected $class;

    /** @var string */
    protected $color;

    /** @var string|null */
    protected $legend;

    /** @var mixed */
    protected $primaryField;

    /** @var mixed */
    protected $entity;

    /** @var EntityRepository */
    protected $repository;

    public function __construct()
    {
        $this->fields = [];
        $this->color = '';
    }

    /**
     * @param EntityRepository $repository
     */
    public function setRepository(EntityRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * @return null|object
     */
    public function getEntity()
    {
        $primaryKeyValue = current($this->getPrimaryKeyValue());

        return $this->repository->find($primaryKeyValue);
    }

    /**
     * @return array
     */
    public function getPrimaryKeyValue()
    {
        $primaryFieldValue = $this->getPrimaryFieldValue();

        if (is_array($primaryFieldValue)) {
            return $primaryFieldValue;
        }

        // @todo: is that correct? shouldn't be [$this->primaryField => $primaryFieldValue] ??
        return ['id' => $primaryFieldValue];
    }

    /**
     * @throws \InvalidArgumentException
     *
     * @return array|mixed
     */
    public function getPrimaryFieldValue()
    {
        if (null === $this->primaryField) {
            throw new \InvalidArgumentException('Primary column must be defined');
        }

        if (is_array($this->primaryField)) {
            return array_intersect_key($this->fields, array_flip($this->primaryField));
        }

        if (!isset($this->fields[$this->primaryField])) {
            throw new \InvalidArgumentException('Primary field not added to fields');
        }

        return $this->fields[$this->primaryField];
    }

    /**
     * @param mixed $primaryField
     *
     * @return $this
     */
    public function setPrimaryField($primaryField)
    {
        $this->primaryField = $primaryField;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getPrimaryField()
    {
        return $this->primaryField;
    }

    /**
     * @param mixed $columnId
     * @param mixed $value
     *
     * @return $this
     */
    public function setField($columnId, $value)
    {
        $this->fields[$columnId] = $value;

        return $this;
    }

    /**
     * @param mixed $columnId
     *
     * @return mixed
     */
    public function getField($columnId)
    {
        return isset($this->fields[$columnId]) ? $this->fields[$columnId] : '';
    }

    /**
     * @param string $class
     *
     * @return $this
     */
    public function setClass($class)
    {
        $this->class = $class;

        return $this;
    }

    /**
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     * @param string $color
     *
     * @return $this
     */
    public function setColor($color)
    {
        $this->color = $color;

        return $this;
    }

    /**
     * @return string
     */
    public function getColor()
    {
        return $this->color;
    }

    /**
     * @param string $legend
     *
     * @return $this
     */
    public function setLegend($legend)
    {
        $this->legend = $legend;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getLegend()
    {
        return $this->legend;
    }
}


<?php

/*
 * This file is part of the DataGridBundle.
 *
 * (c) Abhoryo <abhoryo@free.fr>
 * (c) Stanislav Turza
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace APY\DataGridBundle\Grid\Source;

use APY\DataGridBundle\Grid\Column\ArrayColumn;
use APY\DataGridBundle\Grid\Column\BooleanColumn;
use APY\DataGridBundle\Grid\Column\Column;
use APY\DataGridBundle\Grid\Column\DateColumn;
use APY\DataGridBundle\Grid\Column\DateTimeColumn;
use APY\DataGridBundle\Grid\Column\NumberColumn;
use APY\DataGridBundle\Grid\Column\TextColumn;
use APY\DataGridBundle\Grid\Column\UntypedColumn;
use APY\DataGridBundle\Grid\Rows;

/**
 * Vector is really an Array.
 *
 * @author dellamowica
 */
class Vector extends Source
{
    /**
     * @var array
     */
    protected $data = [];

    /**
     * either a column name as a string
     *  or an array of names of columns.
     *
     * @var mixed
     */
    protected $id = null;

    /**
     * Array of columns.
     *
     * @var Column[]
     */
    protected $columns;

    /**
     * Creates the Vector and sets its data.
     *
     * @param array $data
     * @param array $columns
     */
    public function __construct(array $data, array $columns = [])
    {
        if (!empty($data)) {
            $this->setData($data);
        }

        $this->setColumns($columns);
    }

    public function initialise($container)
    {
        if (!empty($this->data)) {
            $this->guessColumns();
        }
    }

    protected function guessColumns()
    {
        $guessedColumns = [];
        $dataColumnIds = array_keys(reset($this->data));

        foreach ($dataColumnIds as $id) {
            if (!$this->hasColumn($id)) {
                $params = [
                    'id'         => $id,
                    'title'      => $id,
                    'source'     => true,
                    'filterable' => true,
                    'sortable'   => true,
                    'visible'    => true,
                    'field'      => $id,
                ];
                $guessedColumns[] = new UntypedColumn($params);
            }
        }

        $this->setColumns(array_merge($this->columns, $guessedColumns));

        // Guess on the first 10 rows only
        $iteration = min(10, count($this->data));

        foreach ($this->columns as $c) {
            if (!$c instanceof UntypedColumn) {
                continue;
            }

            $i = 0;
            $fieldTypes = [];

            foreach ($this->data as $row) {
                if (!isset($row[$c->getId()])) {
                    continue;
                }

                $fieldValue = $row[$c->getId()];

                if ($fieldValue !== '' && $fieldValue !== null) {
                    if (is_array($fieldValue)) {
                        $fieldTypes['array'] = 1;
                    } elseif ($fieldValue instanceof \DateTime) {
                        if ($fieldValue->format('His') === '000000') {
                            $fieldTypes['date'] = 1;
                        } else {
                            $fieldTypes['datetime'] = 1;
                        }
                    } elseif (strlen($fieldValue) >= 3 && strtotime($fieldValue) !== false) {
                        $dt = new \DateTime($fieldValue);
                        if ($dt->format('His') === '000000') {
                            $fieldTypes['date'] = 1;
                        } else {
                            $fieldTypes['datetime'] = 1;
                        }
                    } elseif (true === $fieldValue || false === $fieldValue || 1 === $fieldValue || 0 === $fieldValue || '1' === $fieldValue || '0' === $fieldValue) {
                        $fieldTypes['boolean'] = 1;
                    } elseif (is_numeric($fieldValue)) {
                        $fieldTypes['number'] = 1;
                    } else {
                        $fieldTypes['text'] = 1;
                    }
                }

                if (++$i >= $iteration) {
                    break;
                }
            }

            if (count($fieldTypes) == 1) {
                $c->setType(key($fieldTypes));
            } elseif (isset($fieldTypes['boolean']) && isset($fieldTypes['number'])) {
                $c->setType('number');
            } elseif (isset($fieldTypes['date']) && isset($fieldTypes['datetime'])) {
                $c->setType('datetime');
            } else {
                $c->setType('text');
            }
        }
    }

    /**
     * @param \APY\DataGridBundle\Grid\Columns $columns
     */
    public function getColumns($columns)
    {
        $token = empty($this->id); //makes the first column primary by default

        foreach ($this->columns as $c) {
            if ($c instanceof UntypedColumn) {
                switch ($c->getType()) {
                    case 'date':
                        $column = new DateColumn($c->getParams());
                        break;
                    case 'datetime':
                        $column = new DateTimeColumn($c->getParams());
                        break;
                    case 'boolean':
                        $column = new BooleanColumn($c->getParams());
                        break;
                    case 'number':
                        $column = new NumberColumn($c->getParams());
                        break;
                    case 'array':
                        $column = new ArrayColumn($c->getParams());
                        break;
                    case 'text':
                    default:
                        $column = new TextColumn($c->getParams());
                        break;
                }
            } else {
                $column = $c;
            }

            if (!$column->isPrimary()) {
                $column->setPrimary((is_array($this->id) && in_array($column->getId(), $this->id)) || $column->getId() == $this->id || $token);
            }

            $columns->addColumn($column);

            $token = false;
        }
    }

    /**
     * @param \APY\DataGridBundle\Grid\Column\Column[] $columns
     * @param int                                      $page             Page Number
     * @param int                                      $limit            Rows Per Page
     * @param int                                      $maxResults       Max rows
     * @param int                                      $gridDataJunction Grid data junction
     *
     * @return Rows
     */
    public function execute($columns, $page = 0, $limit = 0, $maxResults = null, $gridDataJunction = Column::DATA_CONJUNCTION)
    {
        return $this->executeFromData($columns, $page, $limit, $maxResults);
    }

    public function populateSelectFilters($columns, $loop = false)
    {
        $this->populateSelectFiltersFromData($columns, $loop);
    }

    public function getTotalCount($maxResults = null)
    {
        return $this->getTotalCountFromData($maxResults);
    }

    public function getHash()
    {
        return __CLASS__ . md5(implode('', array_map(function ($c) { return $c->getId(); }, $this->columns)));
    }

    /**
     * sets the primary key.
     *
     * @param mixed $id either a string or an array of strings
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set a two-dimentional array.
     *
     * @param array $data
     *
     * @throws \InvalidArgumentException
     */
    public function setData($data)
    {
        $this->data = $data;

        if (!is_array($this->data) || empty($this->data)) {
            throw new \InvalidArgumentException('Data should be an array with content');
        }

        // This seems to exclude ...
        if (is_object(reset($this->data))) {
            foreach ($this->data as $key => $object) {
                $this->data[$key] = (array) $object;
            }
        }

        // ... this other (or vice versa)
        $firstRaw = reset($this->data);
        if (!is_array($firstRaw) || empty($firstRaw)) {
            throw new \InvalidArgumentException('Data should be a two-dimentional array');
        }
    }

    public function delete(array $ids)
    {
    }

    protected function setColumns($columns)
    {
        $this->columns = $columns;
    }

    protected function hasColumn($id)
    {
        foreach ($this->columns as $c) {
            if ($id === $c->getId()) {
                return true;
            }
        }

        return false;
    }
}


<?php

namespace APY\DataGridBundle\Grid;

use APY\DataGridBundle\Grid\Column\Column;
use APY\DataGridBundle\Grid\Exception\UnexpectedTypeException;
use APY\DataGridBundle\Grid\Source\Source;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Class GridFactory.
 *
 * @author  Quentin Ferrer
 */
class GridFactory implements GridFactoryInterface
{
    /**
     * The service container.
     *
     * @var Container
     */
    private $container;

    /**
     * @var GridRegistryInterface
     */
    private $registry;

    /**
     * Constructor.
     *
     * @param Container             $container The service container
     * @param GridRegistryInterface $registry  The grid registry
     */
    public function __construct(Container $container, GridRegistryInterface $registry)
    {
        $this->container = $container;
        $this->registry = $registry;
    }

    /**
     * {@inheritdoc}
     */
    public function create($type = null, Source $source = null, array $options = [])
    {
        return $this->createBuilder($type, $source, $options)->getGrid();
    }

    /**
     * {@inheritdoc}
     */
    public function createBuilder($type = 'grid', Source $source = null, array $options = [])
    {
        $type = $this->resolveType($type);
        $options = $this->resolveOptions($type, $source, $options);

        $builder = new GridBuilder($this->container, $this, $type->getName(), $options);
        $builder->setType($type);

        $type->buildGrid($builder, $options);

        return $builder;
    }

    /**
     * {@inheritdoc}
     */
    public function createColumn($name, $type, array $options = [])
    {
        if (!$type instanceof Column) {
            if (!is_string($type)) {
                throw new UnexpectedTypeException($type, 'string, APY\DataGridBundle\Grid\Column\Column');
            }

            $column = clone $this->registry->getColumn($type);

            $column->__initialize(array_merge([
                'id'     => $name,
                'title'  => $name,
                'field'  => $name,
                'source' => true,
            ], $options));
        } else {
            $column = $type;
            $column->setId($name);
        }

        return $column;
    }

    /**
     * Returns an instance of type.
     *
     * @param string|GridTypeInterface $type The type of the grid
     *
     * @return GridTypeInterface
     */
    private function resolveType($type)
    {
        if (!$type instanceof GridTypeInterface) {
            if (!is_string($type)) {
                throw new UnexpectedTypeException($type, 'string, APY\DataGridBundle\Grid\GridTypeInterface');
            }

            $type = $this->registry->getType($type);
        }

        return $type;
    }

    /**
     * Returns the options resolved.
     *
     * @param GridTypeInterface $type
     * @param Source            $source
     * @param array             $options
     *
     * @return array
     */
    private function resolveOptions(GridTypeInterface $type, Source $source = null, array $options = [])
    {
        $resolver = new OptionsResolver();

        $type->configureOptions($resolver);

        if (null !== $source && !isset($options['source'])) {
            $options['source'] = $source;
        }

        $options = $resolver->resolve($options);

        return $options;
    }
}


<?php

/*
 * This file is part of the DataGridBundle.
 *
 * (c) Abhoryo <abhoryo@free.fr>
 * (c) Stanislav Turza
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 *
 */

namespace APY\DataGridBundle\Grid\Source;

use APY\DataGridBundle\Grid\Column\Column;
use APY\DataGridBundle\Grid\Helper\ColumnsIterator;
use APY\DataGridBundle\Grid\Row;
use APY\DataGridBundle\Grid\Rows;
use Doctrine\ODM\MongoDB\Query\Builder as QueryBuilder;
use MongoDB\BSON\Regex;

class Document extends Source
{
    /**
     * @var \Doctrine\ODM\MongoDB\Query\Builder;
     */
    protected $query;

    /**
     * @var \Doctrine\ODM\MongoDB\DocumentManager
     */
    protected $manager;

    /**
     * e.g. Base\Cms\Document\Page.
     */
    protected $class;

    /**
     * @var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata
     */
    protected $odmMetadata;

    /**
     * e.g. Cms:Page.
     */
    protected $documentName;

    /**
     * @var \APY\DataGridBundle\Grid\Mapping\Metadata\Metadata
     */
    protected $metadata;

    /**
     * @var int Items count
     */
    protected $count;

    /**
     * @var string
     */
    protected $group;

    /**
     * @var array
     */
    protected $referencedColumns = [];

    /**
     * @var array
     */
    protected $referencedMappings = [];

    /**
     * @param string $documentName e.g. "Cms:Page"
     */
    public function __construct($documentName, $group = 'default')
    {
        $this->documentName = $documentName;
        $this->group = $group;
    }

    public function initialise($container)
    {
        $this->manager = $container->get('doctrine.odm.mongodb.document_manager');
        $this->odmMetadata = $this->manager->getClassMetadata($this->documentName);
        $this->class = $this->odmMetadata->getReflectionClass()->getName();

        $mapping = $container->get('grid.mapping.manager');
        $mapping->addDriver($this, -1);
        $this->metadata = $mapping->getMetadata($this->class, $this->group);
    }

    /**
     * @param \APY\DataGridBundle\Grid\Columns $columns
     */
    public function getColumns($columns)
    {
        foreach ($this->metadata->getColumnsFromMapping($columns) as $column) {
            $columns->addColumn($column);
        }
    }

    protected function normalizeOperator($operator)
    {
        switch ($operator) {
            // For case insensitive
            case Column::OPERATOR_EQ:
            case Column::OPERATOR_LIKE:
            case Column::OPERATOR_NLIKE:
            case Column::OPERATOR_RLIKE:
            case Column::OPERATOR_LLIKE:
            case Column::OPERATOR_SLIKE:
            case Column::OPERATOR_NSLIKE:
            case Column::OPERATOR_RSLIKE:
            case Column::OPERATOR_LSLIKE:
            case Column::OPERATOR_NEQ:
                return 'equals';
            case Column::OPERATOR_ISNULL:
            case Column::OPERATOR_ISNOTNULL:
                return 'exists';
            default:
                return $operator;
        }
    }

    protected function normalizeValue($operator, $value)
    {
        switch ($operator) {
            case Column::OPERATOR_NEQ:
                return new Regex('^(?!' . $value . '$).*$', 'i');
            case Column::OPERATOR_LIKE:
                return new Regex($value, 'i');
            case Column::OPERATOR_NLIKE:
                return new Regex('^((?!' . $value . ').)*$', 'i');
            case Column::OPERATOR_RLIKE:
                return new Regex('^' . $value, 'i');
            case Column::OPERATOR_LLIKE:
                return new Regex($value . '$', 'i');
            case Column::OPERATOR_SLIKE:
                return new Regex($value, '');
            case Column::OPERATOR_RSLIKE:
                return new Regex('^' . $value, '');
            case Column::OPERATOR_LSLIKE:
                return new Regex($value . '$', '');
            case Column::OPERATOR_ISNULL:
                return false;
            case Column::OPERATOR_ISNOTNULL:
                return true;
            default:
                return $value;
        }
    }

    /**
     * Sets the initial QueryBuilder for this DataGrid.
     *
     * @param QueryBuilder $queryBuilder
     */
    public function initQueryBuilder(QueryBuilder $queryBuilder)
    {
        $this->query = clone $queryBuilder;
    }

    /**
     * @return QueryBuilder
     */
    protected function getQueryBuilder()
    {
        //If a custom QB has been provided, use that
        //Otherwise create our own basic one
        if ($this->query instanceof QueryBuilder) {
            $qb = $this->query;
        } else {
            $qb = $this->query = $this->manager->createQueryBuilder($this->documentName);
        }

        return $qb;
    }

    /**
     * @param ColumnsIterator $columns
     * @param int                                      $page             Page Number
     * @param int                                      $limit            Rows Per Page
     * @param int                                      $gridDataJunction Grid data junction
     *
     * @return \APY\DataGridBundle\Grid\Rows
     */
    public function execute($columns, $page = 0, $limit = 0, $maxResults = null, $gridDataJunction = Column::DATA_CONJUNCTION)
    {
        $this->query = $this->getQueryBuilder();

        $validColumns = [];
        foreach ($columns as $column) {

            //checks if exists '.' notation on referenced columns and build query if it's filtered
            $subColumn = explode('.', $column->getId());
            if (count($subColumn) > 1 && isset($this->referencedMappings[$subColumn[0]])) {
                $this->addReferencedColumnn($subColumn, $column);

                continue;
            }

            $this->query->select($column->getField());

            if ($column->isSorted()) {
                $this->query->sort($column->getField(), $column->getOrder());
            }

            if ($column->isPrimary()) {
                $column->setFilterable(false);
            } elseif ($column->isFiltered()) {
                // Some attributes of the column can be changed in this function
                $filters = $column->getFilters('document');

                foreach ($filters as $filter) {
                    //normalize values
                    $operator = $this->normalizeOperator($filter->getOperator());
                    $value = $this->normalizeValue($filter->getOperator(), $filter->getValue());

                    if ($column->getDataJunction() === Column::DATA_DISJUNCTION) {
                        $this->query->addOr($this->query->expr()->field($column->getField())->$operator($value));
                    } else {
                        $this->query->field($column->getField())->$operator($value);
                    }
                }
            }

            $validColumns[] = $column;
        }

        if ($page > 0) {
            $this->query->skip($page * $limit);
        }

        if ($limit > 0) {
            if ($maxResults !== null && ($maxResults - $page * $limit < $limit)) {
                $limit = $maxResults - $page * $limit;
            }

            $this->query->limit($limit);
        } elseif ($maxResults !== null) {
            $this->query->limit($maxResults);
        }

        //call overridden prepareQuery or associated closure
        $this->prepareQuery($this->query);

        //execute and get results
        $result = new Rows();

        // I really don't know if Cursor is the right type returned (I mean, every single type).
        // As I didn't find out this information, I'm gonna test it with Cursor returned only.
        $cursor = $this->query->getQuery()->execute();

        $this->count = $cursor->count();

        foreach ($cursor as $resource) {
            $row = new Row();
            $properties = $this->getClassProperties($resource);

            foreach ($validColumns as $column) {
                if (isset($properties[strtolower($column->getId())])) {
                    $row->setField($column->getId(), $properties[strtolower($column->getId())]);
                }
            }

            $this->addReferencedFields($row, $resource);

            //call overridden prepareRow or associated closure
            if (($modifiedRow = $this->prepareRow($row)) !== null) {
                $result->addRow($modifiedRow);
            }
        }

        return $result;
    }

    /**
     * @param array $subColumn
     * @param Column \APY\DataGridBundle\Grid\Column\Column
     */
    protected function addReferencedColumnn(array $subColumn, Column $column)
    {
        $this->referencedColumns[$subColumn[0]][] = $subColumn[1];

        if ($column->isFiltered()) {
            $helperQuery = $this->manager->createQueryBuilder($this->referencedMappings[$subColumn[0]]);
            $filters = $column->getFilters('document');
            foreach ($filters as $filter) {
                $operator = $this->normalizeOperator($filter->getOperator());
                $value = $this->normalizeValue($filter->getOperator(), $filter->getValue());

                $helperQuery->field($subColumn[1])->$operator($value);
                $this->prepareQuery($this->query);

                $cursor = $helperQuery->getQuery()->execute();

                foreach ($cursor as $resource) {
                    // Is this case possible? I don't think so
                    if ($cursor->count() > 0) {
                        $this->query->select($subColumn[0]);
                    }

                    if ($cursor->count() == 1) {
                        $this->query->field($subColumn[0])->references($resource);
                    } else {
                        $this->query->addOr($this->query->expr()->field($subColumn[0])->references($resource));
                    }
                }
            }
        }
    }

    /**
     * @param \APY\DataGridBundle\Grid\Row $row
     * @param Document                     $resource
     *
     * @throws \Exception if getter for field does not exists
     *
     * @return \APY\DataGridBundle\Grid\Row $row with referenced fields
     */
    protected function addReferencedFields(Row $row, $resource)
    {
        foreach ($this->referencedColumns as $parent => $subColumns) {
            $node = $this->getClassProperties($resource);
            if (isset($node[strtolower($parent)])) {
                $node = $node[strtolower($parent)];

                foreach ($subColumns as $field) {
                    $getter = 'get' . ucfirst($field);
                    if (method_exists($node, $getter)) {
                        $row->setField($parent . '.' . $field, $node->$getter());
                    } else {
                        throw new \Exception(sprintf('Method %s for Document %s not exists', $getter, $this->referencedMappings[$parent]));
                    }
                }
            }
        }

        return $row;
    }

    public function getTotalCount($maxResults = null)
    {
        if ($maxResults !== null) {
            return min([$maxResults, $this->count]);
        }

        return $this->count;
    }

    protected function getClassProperties($obj)
    {
        $reflect = new \ReflectionClass($obj);
        $props = $reflect->getProperties();
        $result = [];

        foreach ($props as $property) {
            $property->setAccessible(true);
            $result[strtolower($property->getName())] = $property->getValue($obj);
        }

        return $result;
    }

    /**
     * @param string $class
     * @param string $group
     *
     * @return array
     */
    public function getFieldsMetadata($class, $group = 'default')
    {
        $result = [];
        foreach ($this->odmMetadata->getReflectionProperties() as $property) {
            $name = $property->getName();
            $mapping = $this->odmMetadata->getFieldMapping($name);
            $values = ['title' => $name, 'source' => true];

            if (isset($mapping['fieldName'])) {
                $values['field'] = $mapping['fieldName'];
                $values['id'] = $mapping['fieldName'];
            }

            if (isset($mapping['id']) && $mapping['id'] == 'id') {
                $values['primary'] = true;
            }

            switch ($mapping['type']) {
                case 'id':
                case 'string':
                case 'bin_custom':
                case 'bin_func':
                case 'bin_md5':
                case 'bin':
                case 'bin_uuid':
                case 'file':
                case 'key':
                case 'increment':
                    $values['type'] = 'text';
                    break;
                case 'int':
                case 'float':
                    $values['type'] = 'number';
                    break;
                /*case 'hash':
                $values['type'] = 'array';*/
                case 'boolean':
                    $values['type'] = 'boolean';
                    break;
                case 'date':
                case 'timestamp':
                    $values['type'] = 'date';
                    break;
                case 'collection':
                    $values['type'] = 'array';
                    break;
                case 'one':
                    $values['type'] = 'array';
                    if (isset($mapping['reference']) && $mapping['reference'] === true) {
                        $this->referencedMappings[$name] = $mapping['targetDocument'];
                    }
                    break;
                case 'many':
                    $values['type'] = 'array';
                    break;
                default:
                    $values['type'] = 'text';
            }

            $result[$name] = $values;
        }

        return $result;
    }

    public function populateSelectFilters($columns, $loop = false)
    {
        $queryFromSource = $this->getQueryBuilder();
        $queryFromQuery = clone $this->query;

        // Clean the select fields from the query
        foreach ($columns as $column) {
            $queryFromQuery->exclude($column->getField());
        }

        /* @var $column Column */
        foreach ($columns as $column) {
            $selectFrom = $column->getSelectFrom();

            if ($column->getFilterType() === 'select' && ($selectFrom === 'source' || $selectFrom === 'query')) {

                // For negative operators, show all values
                if ($selectFrom === 'query') {
                    foreach ($column->getFilters('document') as $filter) {
                        if (in_array($filter->getOperator(), [Column::OPERATOR_NEQ, Column::OPERATOR_NLIKE, Column::OPERATOR_NSLIKE])) {
                            $selectFrom = 'source';
                            break;
                        }
                    }
                }

                // Dynamic from query or not ?
                $query = ($selectFrom === 'source') ? clone $queryFromSource : clone $queryFromQuery;

                $result = $query->select($column->getField())
                    ->distinct($column->getField())
                    ->sort($column->getField(), 'asc')
                    ->skip(null)
                    ->limit(null)
                    ->getQuery()
                    ->execute();

                $values = [];
                foreach ($result as $value) {
                    switch ($column->getType()) {
                        case 'number':
                            $values[$value] = $column->getDisplayedValue($value);
                            break;
                        case 'datetime':
                        case 'date':
                        case 'time':
                            if ($value instanceof \MongoDate || $value instanceof \MongoTimestamp) {
                                $value = $value->sec;
                            }

                            // Mongodb bug ? timestamp value is on the key 'i' instead of the key 't'
                            if (is_array($value) && array_keys($value) == ['t', 'i']) {
                                $value = $value['i'];
                            }

                            $displayedValue = $column->getDisplayedValue($value);
                            $values[$displayedValue] = $displayedValue;
                            break;
                        default:
                            $values[$value] = $value;
                    }
                }

                // It avoids to have no result when the other columns are filtered
                if ($selectFrom === 'query' && empty($values) && $loop === false) {
                    $column->setSelectFrom('source');
                    $this->populateSelectFilters($columns, true);
                } else {
                    $values = $this->prepareColumnValues($column, $values);
                    $column->setValues($values);
                }
            }
        }
    }

    /**
     * @param array $ids
     *
     * @throws \Exception
     */
    public function delete(array $ids)
    {
        $repository = $this->getRepository();

        foreach ($ids as $id) {
            $object = $repository->find($id);

            if (!$object) {
                throw new \Exception(sprintf('No %s found for id %s', $this->documentName, $id));
            }

            $this->manager->remove($object);
        }

        $this->manager->flush();
    }

    /**
     * @return \Doctrine\ODM\MongoDB\DocumentRepository
     */
    public function getRepository()
    {
        return $this->manager->getRepository($this->documentName);
    }

    /**
     * @return string
     */
    public function getHash()
    {
        return $this->documentName;
    }
}


<?php

namespace APY\DataGridBundle\Grid;

use APY\DataGridBundle\Grid\Column\Column;
use APY\DataGridBundle\Grid\Exception\InvalidArgumentException;
use APY\DataGridBundle\Grid\Exception\UnexpectedTypeException;
use Symfony\Component\DependencyInjection\Container;

/**
 * A builder for creating Grid instances.
 *
 * @author  Quentin Ferrer
 */
class GridBuilder extends GridConfigBuilder implements GridBuilderInterface
{
    /**
     * The container.
     *
     * @var Container
     */
    private $container;

    /**
     * The factory.
     *
     * @var GridFactoryInterface
     */
    private $factory;

    /**
     * Columns of the grid builder.
     *
     * @var Column[]
     */
    private $columns = [];

    /**
     * Constructor.
     *
     * @param Container            $container The service container
     * @param GridFactoryInterface $factory   The grid factory
     * @param string               $name      The name of the grid
     * @param array                $options   The options of the grid
     */
    public function __construct(Container $container, GridFactoryInterface $factory, $name, array $options = [])
    {
        parent::__construct($name, $options);

        $this->container = $container;
        $this->factory = $factory;
    }

    /**
     * {@inheritdoc}
     */
    public function add($name, $type, array $options = [])
    {
        if (!$type instanceof Column) {
            if (!is_string($type)) {
                throw new UnexpectedTypeException($type, 'string, APY\DataGridBundle\Grid\Column\Column');
            }

            $type = $this->factory->createColumn($name, $type, $options);
        }

        $this->columns[$name] = $type;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function get($name)
    {
        if (!$this->has($name)) {
            throw new InvalidArgumentException(sprintf('The column with the name "%s" does not exist.', $name));
        }

        $column = $this->columns[$name];

        return $column;
    }

    /**
     * {@inheritdoc}
     */
    public function has($name)
    {
        return isset($this->columns[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function remove($name)
    {
        unset($this->columns[$name]);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getGrid()
    {
        $config = $this->getGridConfig();

        $grid = new Grid($this->container, $config->getName(), $config);

        foreach ($this->columns as $column) {
            $grid->addColumn($column);
        }

        if (!empty($this->actions)) {
            foreach ($this->actions as $columnId => $actions) {
                foreach ($actions as $action) {
                    $grid->addRowAction($action);
                }
            }
        }

        $grid->initialize();

        return $grid;
    }
}


<?php

/*
 * This file is part of the DataGridBundle.
 *
 * (c) Abhoryo <abhoryo@free.fr>
 * (c) Stanislav Turza
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace APY\DataGridBundle\Twig;

use APY\DataGridBundle\Grid\Grid;
use Pagerfanta\Adapter\NullAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Component\Routing\RouterInterface;
use Twig_Environment;
use Twig_Extension;
use Twig_Extension_GlobalsInterface;
use Twig_SimpleFunction;
use Twig_Template;

/**
 * DataGrid Twig Extension.
 *
 * (c) Abhoryo <abhoryo@free.fr>
 * (c) Stanislav Turza
 *
 * Updated by Nicolas Claverie <info@artscore-studio.fr>
 */
class DataGridExtension extends Twig_Extension implements Twig_Extension_GlobalsInterface
{
    const DEFAULT_TEMPLATE = 'APYDataGridBundle::blocks.html.twig';

    /**
     * @var Twig_Template[]
     */
    protected $templates = [];

    /**
     * @var string
     */
    protected $theme;

    /**
     * @var RouterInterface
     */
    protected $router;

    /**
     * @var array
     */
    protected $names;

    /**
     * @var array
     */
    protected $params = [];

    /**
     * @var array
     */
    protected $pagerFantaDefs;

    /**
     * @var string
     */
    protected $defaultTemplate;

    /**
     * @param RouterInterface $router
     * @param string          $defaultTemplate
     */
    public function __construct($router, $defaultTemplate)
    {
        $this->router = $router;
        $this->defaultTemplate = $defaultTemplate;
    }

    /**
     * @param array $def
     */
    public function setPagerFanta(array $def)
    {
        $this->pagerFantaDefs = $def;
    }

    /**
     * @return array
     */
    public function getGlobals()
    {
        return [
            'grid'           => null,
            'column'         => null,
            'row'            => null,
            'value'          => null,
            'submitOnChange' => null,
            'withjs'         => true,
            'pagerfanta'     => false,
            'op'             => 'eq',
        ];
    }

    /**
     * Returns a list of functions to add to the existing list.
     *
     * @return array An array of functions
     */
    public function getFunctions()
    {
        return [
            new Twig_SimpleFunction('grid', [$this, 'getGrid'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_html', [$this, 'getGridHtml'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_url', [$this, 'getGridUrl'], [
                'is_safe' => ['html'],
            ]),
            new Twig_SimpleFunction('grid_filter', [$this, 'getGridFilter'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_column_operator', [$this, 'getGridColumnOperator'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_cell', [$this, 'getGridCell'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_search', [$this, 'getGridSearch'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_pager', [$this, 'getGridPager'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
            new Twig_SimpleFunction('grid_pagerfanta', [$this, 'getPagerfanta'], [
                'is_safe' => ['html'],
            ]),
            new Twig_SimpleFunction('grid_*', [$this, 'getGrid_'], [
                'needs_environment' => true,
                'is_safe'           => ['html'],
            ]),
        ];
    }

    /**
     * @param unknown $grid
     * @param unknown $theme
     * @param string  $id
     * @param array   $params
     */
    public function initGrid($grid, $theme = null, $id = '', array $params = [])
    {
        $this->theme = $theme;
        $this->templates = [];

        $this->names[$grid->getHash()] = ($id == '') ? $grid->getId() : $id;
        $this->params = $params;
    }

    /**
     * Render grid block.
     *
     * @param Twig_Environment              $environment
     * @param \APY\DataGridBundle\Grid\Grid $grid
     * @param string                        $theme
     * @param string                        $id
     *
     * @return string
     */
    public function getGrid(Twig_Environment $environment, $grid, $theme = null, $id = '', array $params = [], $withjs = true)
    {
        $this->initGrid($grid, $theme, $id, $params);

        // For export
        $grid->setTemplate($theme);

        return $this->renderBlock($environment, 'grid', ['grid' => $grid, 'withjs' => $withjs]);
    }

    /**
     * Render grid block (html only).
     *
     * @param Twig_Environment              $environment
     * @param \APY\DataGridBundle\Grid\Grid $grid
     * @param string                        $theme
     * @param string                        $id
     *
     * @return string
     */
    public function getGridHtml(Twig_Environment $environment, $grid, $theme = null, $id = '', array $params = [])
    {
        return $this->getGrid($environment, $grid, $theme, $id, $params, false);
    }

    /**
     * @param Twig_Environment $environment
     * @param string           $name
     * @param unknown          $grid
     *
     * @return string
     */
    public function getGrid_(Twig_Environment $environment, $name, $grid)
    {
        return $this->renderBlock($environment, 'grid_' . $name, ['grid' => $grid]);
    }

    /**
     * @param Twig_Environment $environment
     * @param unknown          $grid
     *
     * @return string
     */
    public function getGridPager(Twig_Environment $environment, $grid)
    {
        return $this->renderBlock($environment, 'grid_pager', ['grid' => $grid, 'pagerfanta' => $this->pagerFantaDefs['enable']]);
    }

    /**
     * Cell Drawing override.
     *
     * @param Twig_Environment                       $environment
     * @param \APY\DataGridBundle\Grid\Column\Column $column
     * @param \APY\DataGridBundle\Grid\Row           $row
     * @param \APY\DataGridBundle\Grid\Grid          $grid
     *
     * @return string
     */
    public function getGridCell(Twig_Environment $environment, $column, $row, $grid)
    {
        $value = $column->renderCell($row->getField($column->getId()), $row, $this->router);

        $id = $this->names[$grid->getHash()];

        if (($id != '' && ($this->hasBlock($environment, $block = 'grid_' . $id . '_column_' . $column->getRenderBlockId() . '_cell')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_' . $column->getType() . '_cell')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_' . $column->getParentType() . '_cell')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_id_' . $column->getRenderBlockId() . '_cell')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_type_' . $column->getType() . '_cell')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_type_' . $column->getParentType() . '_cell')))
            || $this->hasBlock($environment, $block = 'grid_column_' . $column->getRenderBlockId() . '_cell')
            || $this->hasBlock($environment, $block = 'grid_column_' . $column->getType() . '_cell')
            || $this->hasBlock($environment, $block = 'grid_column_' . $column->getParentType() . '_cell')
            || $this->hasBlock($environment, $block = 'grid_column_id_' . $column->getRenderBlockId() . '_cell')
            || $this->hasBlock($environment, $block = 'grid_column_type_' . $column->getType() . '_cell')
            || $this->hasBlock($environment, $block = 'grid_column_type_' . $column->getParentType() . '_cell')
        ) {
            return $this->renderBlock($environment, $block, ['grid' => $grid, 'column' => $column, 'row' => $row, 'value' => $value]);
        }

        return $this->renderBlock($environment, 'grid_column_cell', ['grid' => $grid, 'column' => $column, 'row' => $row, 'value' => $value]);
    }

    /**
     * Filter Drawing override.
     *
     * @param Twig_Environment                       $environment
     * @param \APY\DataGridBundle\Grid\Column\Column $column
     * @param \APY\DataGridBundle\Grid\Grid          $grid
     *
     * @return string
     */
    public function getGridFilter(Twig_Environment $environment, $column, $grid, $submitOnChange = true)
    {
        $id = $this->names[$grid->getHash()];

        if (($id != '' && ($this->hasBlock($environment, $block = 'grid_' . $id . '_column_' . $column->getRenderBlockId() . '_filter')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_id_' . $column->getRenderBlockId() . '_filter')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_type_' . $column->getType() . '_filter')
                    || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_type_' . $column->getParentType() . '_filter'))
                || $this->hasBlock($environment, $block = 'grid_' . $id . '_column_filter_type_' . $column->getFilterType()))
            || $this->hasBlock($environment, $block = 'grid_column_' . $column->getRenderBlockId() . '_filter')
            || $this->hasBlock($environment, $block = 'grid_column_id_' . $column->getRenderBlockId() . '_filter')
            || $this->hasBlock($environment, $block = 'grid_column_type_' . $column->getType() . '_filter')
            || $this->hasBlock($environment, $block = 'grid_column_type_' . $column->getParentType() . '_filter')
            || $this->hasBlock($environment, $block = 'grid_column_filter_type_' . $column->getFilterType())
        ) {
            return $this->renderBlock($environment, $block, ['grid' => $grid, 'column' => $column, 'submitOnChange' => $submitOnChange && $column->isFilterSubmitOnChange()]);
        }

        return '';
    }

    /**
     * Column Operator Drawing override.
     *
     * @param Twig_Environment                       $environment
     * @param \APY\DataGridBundle\Grid\Column\Column $column
     * @param \APY\DataGridBundle\Grid\Grid          $grid
     *
     * @return string
     */
    public function getGridColumnOperator(Twig_Environment $environment, $column, $grid, $operator, $submitOnChange = true)
    {
        return $this->renderBlock($environment, 'grid_column_operator', ['grid' => $grid, 'column' => $column, 'submitOnChange' => $submitOnChange, 'op' => $operator]);
    }

    /**
     * @param string                                 $section
     * @param \APY\DataGridBundle\Grid\Grid          $grid
     * @param \APY\DataGridBundle\Grid\Column\Column $param
     *
     * @return string
     */
    public function getGridUrl($section, $grid, $param = null)
    {
        $prefix = $grid->getRouteUrl() . (strpos($grid->getRouteUrl(), '?') ? '&' : '?') . $grid->getHash() . '[';

        switch ($section) {
            case 'order':
                if ($param->isSorted()) {
                    return $prefix . Grid::REQUEST_QUERY_ORDER . ']=' . $param->getId() . '|' . ($param->getOrder() == 'asc' ? 'desc' : 'asc');
                } else {
                    return $prefix . Grid::REQUEST_QUERY_ORDER . ']=' . $param->getId() . '|asc';
                }
            case 'page':
                return $prefix . Grid::REQUEST_QUERY_PAGE . ']=' . $param;
            case 'limit':
                return $prefix . Grid::REQUEST_QUERY_LIMIT . ']=';
            case 'reset':
                return $prefix . Grid::REQUEST_QUERY_RESET . ']=';
            case 'export':
                return $prefix . Grid::REQUEST_QUERY_EXPORT . ']=' . $param;
        }
    }

    /**
     * @param Twig_Environment $environment
     * @param unknown          $grid
     * @param unknown          $theme
     * @param string           $id
     * @param array            $params
     *
     * @return string
     */
    public function getGridSearch(\Twig_Environment $environment, $grid, $theme = null, $id = '', array $params = [])
    {
        $this->initGrid($grid, $theme, $id, $params);

        return $this->renderBlock($environment, 'grid_search', ['grid' => $grid]);
    }

    /**
     * @param unknown $grid
     */
    public function getPagerfanta($grid)
    {
        $adapter = new NullAdapter($grid->getTotalCount());

        $pagerfanta = new Pagerfanta($adapter);
        $pagerfanta->setMaxPerPage($grid->getLimit());
        $pagerfanta->setCurrentPage($grid->getPage() + 1);

        $url = $this->getGridUrl('page', $grid, '');
        $routeGenerator = function ($page) use ($url) {
            return sprintf('%s%d', $url, $page - 1);
        };

        $view = new $this->pagerFantaDefs['view_class']();
        $html = $view->render($pagerfanta, $routeGenerator, $this->pagerFantaDefs['options']);

        return $html;
    }

    /**
     * Render block.
     *
     * @param Twig_Environment $environment
     * @param string           $name
     * @param array            $parameters
     *
     * @throws \InvalidArgumentException If the block could not be found
     *
     * @return string
     */
    protected function renderBlock(Twig_Environment $environment, $name, $parameters)
    {
        foreach ($this->getTemplates($environment) as $template) {
            if ($template->hasBlock($name, [])) {
                return $template->renderBlock($name, array_merge($environment->getGlobals(), $parameters, $this->params));
            }
        }

        throw new \InvalidArgumentException(sprintf('Block "%s" doesn\'t exist in grid template "%s".', $name, $this->theme));
    }

    /**
     * Has block.
     *
     * @param Twig_Environment $environment
     * @param $name string
     *
     * @return bool
     */
    protected function hasBlock(Twig_Environment $environment, $name)
    {
        foreach ($this->getTemplates($environment) as $template) {
            /** @var $template Twig_Template */
            if ($template->hasBlock($name, [])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Template Loader.
     *
     * @param Twig_Environment $environment
     *
     * @throws \Exception
     *
     * @return Twig_Template[]
     */
    protected function getTemplates(Twig_Environment $environment)
    {
        if (empty($this->templates)) {
            if ($this->theme instanceof Twig_Template) {
                $this->templates[] = $this->theme;
                $this->templates[] = $environment->loadTemplate($this->defaultTemplate);
            } elseif (is_string($this->theme)) {
                $this->templates = $this->getTemplatesFromString($environment, $this->theme);
            } elseif ($this->theme === null) {
                $this->templates = $this->getTemplatesFromString($environment, $this->defaultTemplate);
            } else {
                throw new \Exception('Unable to load template');
            }
        }

        return $this->templates;
    }

    /**
     * @param Twig_Environment $environment
     * @param unknown          $theme
     *
     * @return array|Twig_Template[]
     */
    protected function getTemplatesFromString(Twig_Environment $environment, $theme)
    {
        $this->templates = [];

        $template = $environment->loadTemplate($theme);
        while ($template instanceof \Twig_Template) {
            $this->templates[] = $template;
            $template = $template->getParent([]);
        }

        return $this->templates;
    }
}


<?php

/**
 * Doctrine ORM.
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to kontakt@beberlei.de so I can send you a copy immediately.
 */

namespace APY\DataGridBundle\Grid\Helper;

use Doctrine\ORM\Query\AST\AggregateExpression;
use Doctrine\ORM\Query\AST\PathExpression;
use Doctrine\ORM\Query\AST\SelectExpression;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\TreeWalkerAdapter;

/**
 * Replaces the selectClause of the AST with a COUNT statement.
 *
 * @category    DoctrineExtensions
 *
 * @author      David Abdemoulaie <dave@hobodave.com>
 * @copyright   Copyright (c) 2010 David Abdemoulaie (http://hobodave.com/)
 * @license     http://hobodave.com/license.txt New BSD License
 */
class ORMCountWalker extends TreeWalkerAdapter
{
    /**
     * Distinct mode hint name.
     */
    const HINT_DISTINCT = 'doctrine_paginator.distinct';

    /**
     * Walks down a SelectStatement AST node, modifying it to retrieve a COUNT.
     *
     * @param SelectStatement $AST
     *
     * @throws \RuntimeException
     */
    public function walkSelectStatement(SelectStatement $AST)
    {
        $rootComponents = [];
        foreach ($this->_getQueryComponents() as $dqlAlias => $qComp) {
            if (array_key_exists('parent', $qComp) && $qComp['parent'] === null && $qComp['nestingLevel'] == 0) {
                $rootComponents[] = [$dqlAlias => $qComp];
            }
        }

        if (count($rootComponents) > 1) {
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
        }

        $root = reset($rootComponents);
        $parentName = key($root);
        $parent = current($root);

        $pathExpression = new PathExpression(
            PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $parentName,
            $parent['metadata']->getSingleIdentifierFieldName()
        );
        $pathExpression->type = PathExpression::TYPE_STATE_FIELD;

        // Remove the variables which are not used by other clauses
        foreach ($AST->selectClause->selectExpressions as $key => $selectExpression) {
            if ($selectExpression->fieldIdentificationVariable === null) {
                unset($AST->selectClause->selectExpressions[$key]);
            } elseif ($selectExpression->expression instanceof PathExpression) {
                $groupByClause[] = $selectExpression->expression;
            }
        }

        // Put the count expression in the first position
        $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT);
        array_unshift($AST->selectClause->selectExpressions,
            new SelectExpression(
                new AggregateExpression('count', $pathExpression, $distinct), null
            )
        );

        $groupByClause[] = $pathExpression;
        $AST->groupByClause = new \Doctrine\ORM\Query\AST\GroupByClause($groupByClause);

        // ORDER BY is not needed, only increases query execution through unnecessary sorting.
        $AST->orderByClause = null;
    }
}