statamic / ideas

💡Discussions on ideas and feature requests for Statamic
https://statamic.dev
32 stars 1 forks source link

Revive Collection Filter #494

Open aerni opened 3 years ago

aerni commented 3 years ago

Please bring back the collection filter functionality from v2. I've been running into a few cases, where scopes just don't do it. So I had to write a custom tag that extends from the collection tag class and do the filtering in there. But there are things that don't work properly. Like pagination, limit, etc. It's a real headache to reimplement all this. I really miss filters and it's a step back in many ways.

Any reason filters don't exist anymore?

duncanmcclean commented 3 years ago

I'm not sure but I'd imagine it's probably just something that was just never ported across.

jasonvarga commented 3 years ago

They don't exist because we switched to using query builders. We want to be able to work just like Eloquent. If you swapped to using a database, the syntax would be the same.

Filters worked because you would start with all the entries. Potentially very inefficient. Imagine you have a database with 10,000+ entries.

But, I suppose we could still bring them back. You'd just need to be aware of the drawbacks.

aerni commented 3 years ago

Yeah, I understand your point. There might be a possible middle ground though.

What I'm looking for is a way to filter and transform the collection data. With scopes, you can only apply queries. Would it maybe be possible to also be able to transform data in the scope class and return that? Does that make sense?

edalzell commented 3 years ago

Related: https://github.com/statamic/ideas/issues/487

jasonvarga commented 3 years ago

Your request makes sense but it won't be doable in its current state. Same with what Erin just linked to.

The scope doesn't manipulate anything. It happens later on in the tag.

$query = Entry::query();

$yourScope->apply($query); // gets sent to your scope where you can add where clauses, etc.

$query->get(); // this is where all the magic happens and it finally returns Entry instances.

We can't just let you filter afterwards either because it would have been limited and paginated by that point too.

Transforming though, sure. That's probably simple enough. Although you could do that now already.

{{ collection:blog as="entries" }}
  {{ entries a_modifier_that_transforms_them="true" }}
    //
  {{ /entries }}
{{ /collection:blog }}
aerni commented 3 years ago

Maybe there could be a step in between. Something like $query->entries() to get all entries. Do custom magic on there. And then $query->get() at the end to paginate etc.

jasonvarga commented 3 years ago

Can you give a concrete example of what you plan to do?

aerni commented 3 years ago

Sure thing. This is a recent example of a class that used to be a filter in v2 and had to be refactored to a tag in v3.

If the tag has a lat and lng parameter, I need to map through the collection entries and return the transformed result. And apply the limit at the end. Because of the current limitation, I can't easily add pagination.

<?php

namespace App\Tags;

use Illuminate\Support\Collection;
use Statamic\Tags\Collection\Collection as CollectionTag;

class NearbyLibraries extends CollectionTag
{
    /**
     * The {{ nearby_libraries:* }} tag.
     */
    public function __call($method, $args): Collection
    {
        return $this->outputLibraries();
    }

    /**
     * The {{ nearby_libraries }} tag.
     *
     * @return Collection
     */
    public function index(): Collection
    {
        return $this->outputLibraries();
    }

    private function outputLibraries()
    {
        $this->params->put('from', 'libraries');

        $this->limit = $this->params->pull('limit');
        $this->lat = $this->params->get('lat');
        $this->lng = $this->params->get('lng');

        $libraries = parent::index();

        if ($as = $this->params->get('as')) {
            $libraries = $libraries[$as];
        }

        return $this->hasCoordinates() ? $this->nearbyLibraries($libraries) : $libraries->limit($this->limit);
    }

    private function nearbyLibraries($libraries): Collection
    {
        return $libraries->map(function ($library) {
            $distance = $this->haversine($this->lat, $this->lng, $library->get('branch_latitude'), $library->get('branch_longitude'));
            $distanceInMiles = intval(round($distance / 1609));

            $library->set('distance', $distanceInMiles);
            $library->set('distanceLabel', $distanceInMiles . ' mile' . ($distanceInMiles === 1 ? '' : 's'));

            return $library;
        })
        ->sortBy(function ($library) {
            return $library->get('distance');
        })
        ->limit($this->limit);
    }

    private function hasCoordinates(): bool
    {
        if (empty($this->lat)) {
            return false;
        }

        if (empty($this->lng)) {
            return false;
        }

        return true;
    }

    private function haversine(float $latitudeFrom, float $longitudeFrom, float $latitudeTo, float $longitudeTo, int $earthRadius = 6371000): float
    {
        $latFrom = deg2rad($latitudeFrom);
        $lonFrom = deg2rad($longitudeFrom);
        $latTo = deg2rad($latitudeTo);
        $lonTo = deg2rad($longitudeTo);

        $latDelta = $latTo - $latFrom;
        $lonDelta = $lonTo - $lonFrom;

        $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
            cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));

        return $angle * $earthRadius;
    }
}
edalzell commented 3 years ago

Yup, almost exactly the same tag I've written a few times.