1

I have an interface with search function:

interface Searcher
{
    public function search($text,$limit)
}

I have some realization based on API

class APISeach implements Searcher
{
   public function search($text,$limit)
   {
        $params = [
            'name' => $sName,
            'maxRows' => $iLimit,
        ];
        $response = Http::get('http://some_api_service/search', $params)->json();
   }
}

And I have some code which used this Search:

class MyController extends Controller
{
    private $seacher;
    
    public function __construct(Searcher $mySeacher)
    {
         $this->seacher = $mySeacher;
    }

    public function search($text,$limit=10)
    {
         $this->seacher->search($text,$limit)
    }
}

All looks fine (may be). But what if I need change API realization and it will be required another parameters. For example:

class AnotherAPISeach implements Searcher
{
   public function search($text,$language,$fuzzy,$limit)
   {
       $ApiObjectFromLib = new ApiObjectFromLib();
       $response = $ApiObjectFromLib->search($text,$language,$fuzzy,$limit)->json();
   }
}

So it's can not implement Searcher interface any more. Is it exists any way to use interfaces for API functions? All API can required various parameters and it's no good if I need change Interface or Controller code for each API.

3
  • 1
    I would recommend a Criterion type declaration combined with a variadic argument: search(Criterion ...$criteria). This is itself an interface that defines how to integrate a change in criteria to some other query. Commented Jun 30, 2021 at 14:45
  • Jared Farrish - Criterion type it's something new for me. Can you get any link for documentation? Commented Jul 1, 2021 at 7:58
  • It's the Filter/Criteria pattern, where an object describes how to modify a query. I'm surprised Eloquent doesn't implement it (or the similar Specification pattern), so review the Doctrine docs to see what it's about. Commented Jul 1, 2021 at 14:34

2 Answers 2

1

You can use variadic arguments

interface Searcher
{
    public function search($text, $limit, ...$additional);
}

If that defeats the purpose of the interface is up to you to decide 😉 ​​​​​​​

Sign up to request clarification or add additional context in comments.

2 Comments

It kinda does IMHO :)
It can be useful. Thanks.
0

How about something like this (demo)?

public function search(string $name, int $limit = \Search\Limit::DEFAULT)
{
    return $this->api->search(
        new \Search\Name($name),
        new \Search\Limit($limit)
    );
}

public function lookup(
    string $name,
    string $language = \Search\Language::DEFAULT,
    bool $fuzzy = \Search\Fuzzy::DEFAULT,
    int $limit = \Search\Limit::DEFAULT
) {
    return $this->api->search(
        new \Search\Name($name),
        new \Search\Language($language),
        new \Search\Fuzzy($fuzzy),
        new \Search\Limit($limit)
    );
}

These "specifications" look like this:

namespace Search
{
    interface Specification
    {
        public function __invoke(array $params): array;
    }
    
    class Name implements Specification
    {
        private $name = null;
        
        public function __construct(string $name)
        {
            $this->name = $name;
        }
        
        public function __invoke(array $params): array
        {
            return [
                'name' => $this->name,
            ];
        }
    }
    
    class Language implements Specification
    {
        const DEFAULT = 'en';
        
        private $language = null;
        
        public function __construct(string $language)
        {
            $this->language = $language;
        }
        
        public function __invoke(array $params): array
        {
            return [
                'language' => $this->language ?? 'en',
            ];
        }
    }
    
    class Fuzzy implements Specification
    {
        const DEFAULT = true;
        
        private $fuzzy = null;
        
        public function __construct(bool $fuzzy)
        {
            $this->fuzzy = $fuzzy;
        }
        
        public function __invoke(array $params): array
        {
            return [
                'fuzzy' => $this->fuzzy,
            ];
        }
    }
    
    class Limit implements Specification
    {
        const DEFAULT = 10;
        
        private $max = null;
        
        public function __construct(int $limit)
        {
            $this->limit = $limit;
        }
        
        public function __invoke(array $params): array
        {
            return [
                'maxRows' => $this->limit ?: self::DEFAULT,
            ];
        }
    }
}

Which the searchable API composes like this:

interface Searchable
{
    public function search(\Search\Specification... $criteria);
}

class Search implements Searchable
{
    private $url = '/search';
    
    private $defaults = [
        'maxRows' => \Search\Limit::DEFAULT,
    ];
    
    public function search(\Search\Specification ...$criteria)
    {
        return \Http::get($this->url, array_reduce(
            $criteria,
            fn($params, $criteria) => $criteria($params) + $params,
            $this->defaults
        ))->json();
    }
}

The Specification Pattern is interesting, since it implies that a request into a domain is really just a chain of decisions that result in a configuration that can be applied elsewhere.

For instance, note how above the $criteria($params) objects are each given the current $params for the request, for which it may override parameters, read and modify a parameter, or potentially incorporate a Specification check to validate parameters.

Note on the array + array syntax, which is a way to merge arrays:

['foo' => 'bar'] + ['foo' => 'baz'] // left takes precedence: ['foo' => 'bar']

Filter/Criteria is very similar; I tend to think of those having a tighter link to the object it's applied to (Repository, Query or Collection) than Specification, which in my mind applies more to what's to be gotten back.

2 Comments

Wow. Big Thanks for such a detailed answer. This part a little tricky for me fn($params, $criteria) => $criteria($params) + $params, but I got the general concept: you created your own params type (Specification) and used it for variadic argumets. This allows you to create for each argument its own class with its own processing. This way we have as many arguments as we want and each can be modified individually. It is very interesting and I am sure it will be useful for me.
$params is what array_reduce() returns, each specification ($criteria) when invoked returns an array, so the arrow function merges each array (array + array) with left-precedence into $params, which is seeded at first with $this->defaults, another array that can have it's keys overwritten with the reducer's return.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.