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.
Criteriontype 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.