Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHP Fatal Error — Yiisoft\ErrorHandler\Exception\ErrorException Class Yiisoft\Yii\Cycle\Data\Reader\EntityReader contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Yiisoft\Data\Reader\FilterableDataInterface::withFilterHandlers) #562

Closed
rossaddison opened this issue Jan 20, 2023 · 6 comments

Comments

@rossaddison
Copy link
Contributor

What steps will reproduce the problem? Installing the demo

What is the expected result? A clean run

What do you get instead? The above error

Additional info

Q A
Version 1.0.?
PHP version 8.1
Operating system Windows 10
@roxblnfk
Copy link
Member

Related with yiisoft/yii-cycle#150

@rossaddison
Copy link
Contributor Author

rossaddison commented Jan 20, 2023

I notice a couple of namespaces under EntityReader.php need to be adjusted ie.

With reference to composer's "yiisoft/data": "^1.0",

use Yiisoft\Data\Reader\Filter\FilterInterface; <--- to Yiisoft\Data\Reader\FilterInterface ie. remove Filter.

use Yiisoft\Data\Reader\Filter\FilterProcessorInterface; <--- does not exist and needs to be included

The file FilterProcessorInterface.php does not exist under "yiisoft/data": "^1.0" Filter folder, and will need to be included from "yiisoft/data": "^3.0@dev". The package will run after this.

Also:

insert namespace at top of EntityReader.php namely:

use Yiisoft\Data\Reader\FilterHandlerInterface;

and insert following code into EntityReader.php

public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static{}

@rossaddison
Copy link
Contributor Author

FilterProcessorInterface.php

<?php

declare(strict_types=1);

namespace Yiisoft\Data\Reader\Filter;

interface FilterProcessorInterface
{
    public function getOperator(): string;
}

@rossaddison
Copy link
Contributor Author

rossaddison commented Jan 24, 2023

<?php

declare(strict_types=1);

namespace Yiisoft\Yii\Cycle\Data\Reader;

use Closure;
use Cycle\Database\Query\SelectQuery;
use Cycle\ORM\Select;
use Cycle\ORM\Select\QueryBuilder;
use Generator;
use InvalidArgumentException;
use RuntimeException;
use Yiisoft\Data\Reader\DataReaderInterface;
use Yiisoft\Data\Reader\FilterHandlerInterface;
use Yiisoft\Data\Reader\FilterInterface;
use Yiisoft\Data\Reader\Filter\FilterProcessorInterface;
use Yiisoft\Data\Reader\Sort;
use Yiisoft\Yii\Cycle\Data\Reader\Cache\CachedCollection;
use Yiisoft\Yii\Cycle\Data\Reader\Cache\CachedCount;
use Yiisoft\Yii\Cycle\Data\Reader\Processor\QueryBuilderProcessor;

/**
 * @template TKey as array-key
 * @template TValue as array|object
 *
 * @implements DataReaderInterface<TKey, TValue>
 */
final class EntityReader implements DataReaderInterface
{
    private Select|SelectQuery $query;
    private ?int $limit = null;
    private ?int $offset = null;
    private ?Sort $sorting = null;
    private ?FilterInterface $filter = null;
    private CachedCount $countCache;
    private CachedCollection $itemsCache;
    private CachedCollection $oneItemCache;
    /** @var FilterProcessorInterface[]|QueryBuilderProcessor[] */
    private array $filterProcessors = [];

    public function __construct(Select|SelectQuery $query)
    {
        $this->query = clone $query;
        $this->countCache = new CachedCount($this->query);
        $this->itemsCache = new CachedCollection();
        $this->oneItemCache = new CachedCollection();
        $this->setFilterProcessors(
            new Processor\All(),
            new Processor\Any(),
            new Processor\Equals(),
            new Processor\GreaterThan(),
            new Processor\GreaterThanOrEqual(),
            new Processor\In(),
            new Processor\LessThan(),
            new Processor\LessThanOrEqual(),
            new Processor\Like(),
            // new Processor\Not()
        );
    }

    public function getSort(): ?Sort
    {
        return $this->sorting;
    }

    /**
     * @psalm-mutation-free
     */
    public function withLimit(int $limit): static
    {
        if ($limit < 0) {
            throw new InvalidArgumentException('$limit must not be less than 0.');
        }
        $new = clone $this;
        if ($new->limit !== $limit) {
            $new->limit = $limit;
            $new->itemsCache = new CachedCollection();
        }
        return $new;
    }

    /**
     * @psalm-mutation-free
     */
    public function withOffset(int $offset): static
    {
        $new = clone $this;
        if ($new->offset !== $offset) {
            $new->offset = $offset;
            $new->itemsCache = new CachedCollection();
        }
        return $new;
    }

    /**
     * @psalm-mutation-free
     */
    public function withSort(?Sort $sort): static
    {
        $new = clone $this;
        if ($new->sorting !== $sort) {
            $new->sorting = $sort;
            $new->itemsCache = new CachedCollection();
            $new->oneItemCache = new CachedCollection();
        }
        return $new;
    }

    /**
     * @psalm-mutation-free
     */
    public function withFilter(FilterInterface $filter): static
    {
        $new = clone $this;
        if ($new->filter !== $filter) {
            $new->filter = $filter;
            $new->itemsCache = new CachedCollection();
            $new->oneItemCache = new CachedCollection();
        }
        return $new;
    }

    /**
     * @psalm-mutation-free
     */
    public function withFilterProcessors(FilterProcessorInterface ...$filterProcessors): static
    {
        $new = clone $this;
        /** @psalm-suppress ImpureMethodCall */
        $new->setFilterProcessors(...$filterProcessors);
        /** @psalm-suppress ImpureMethodCall */
        $new->resetCountCache();
        $new->itemsCache = new CachedCollection();
        $new->oneItemCache = new CachedCollection();
        return $new;
    }

    public function count(): int
    {
        return $this->countCache->getCount();
    }

    public function read(): iterable
    {
        if ($this->itemsCache->getCollection() === null) {
            $query = $this->buildSelectQuery();
            $this->itemsCache->setCollection($query->fetchAll());
        }
        return $this->itemsCache->getCollection();
    }

    public function readOne(): null|array|object
    {
        if (!$this->oneItemCache->isCollected()) {
            $item = $this->itemsCache->isCollected()
                // get first item from cached collection
                ? $this->itemsCache->getGenerator()->current()
                // read data with limit 1
                : $this->withLimit(1)->getIterator()->current();
            $this->oneItemCache->setCollection($item === null ? [] : [$item]);
        }

        return $this->oneItemCache->getGenerator()->current();
    }

    /**
     * Get Iterator without caching
     */
    public function getIterator(): Generator
    {
        yield from $this->itemsCache->getCollection() ?? $this->buildSelectQuery()->getIterator();
    }

    public function getSql(): string
    {
        $query = $this->buildSelectQuery();
        return (string)($query instanceof Select ? $query->buildQuery() : $query);
    }

    private function setFilterProcessors(FilterProcessorInterface ...$filterProcessors): void
    {
        $processors = [];
        foreach ($filterProcessors as $filterProcessor) {
            if ($filterProcessor instanceof QueryBuilderProcessor) {
                $processors[$filterProcessor->getOperator()] = $filterProcessor;
            }
        }
        $this->filterProcessors = array_merge($this->filterProcessors, $processors);
    }
    
    public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static{}

    private function buildSelectQuery(): SelectQuery|Select
    {
        $newQuery = clone $this->query;
        if ($this->offset !== null) {
            $newQuery->offset($this->offset);
        }
        if ($this->sorting !== null) {
            $newQuery->orderBy($this->normalizeSortingCriteria($this->sorting->getCriteria()));
        }
        if ($this->limit !== null) {
            $newQuery->limit($this->limit);
        }
        if ($this->filter !== null) {
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
        }
        return $newQuery;
    }

    private function makeFilterClosure(FilterInterface $filter): Closure
    {
        return function (QueryBuilder $select) use ($filter) {
            $filterArray = $filter->toArray();
            $operation = array_shift($filterArray);
            $arguments = $filterArray;

            if (!array_key_exists($operation, $this->filterProcessors)) {
                throw new RuntimeException(sprintf('Filter operator "%s" is not supported.', $operation));
            }
            /** @psalm-var QueryBuilderProcessor $processor */
            $processor = $this->filterProcessors[$operation];
            $select->where(...$processor->getAsWhereArguments($arguments, $this->filterProcessors));
        };
    }

    private function resetCountCache(): void
    {
        $newQuery = clone $this->query;
        if ($this->filter !== null) {
            $newQuery->andWhere($this->makeFilterClosure($this->filter));
        }
        $this->countCache = new CachedCount($newQuery);
    }

    private function normalizeSortingCriteria(array $criteria): array
    {
        foreach ($criteria as $field => $direction) {
            if (is_int($direction)) {
                $direction = match ($direction) {
                    SORT_DESC => 'DESC',
                    default => 'ASC',
                };
            }
            $criteria[$field] = $direction;
        }

        return $criteria;
    }
}

@rossaddison
Copy link
Contributor Author

public function withFilterHandlers(FilterHandlerInterface ...$iterableFilterHandlers): static{}

is essential along with

use Yiisoft\Data\Reader\FilterHandlerInterface;

in the namespaces.

@rustamwin
Copy link
Member

Fixed in #566

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants