I've been working towards packaging my domain logic at my current job so I can reuse it in multiple implementations. The code snippets below were an abandoned experiment for a php Laravel project. I have recently put together a simple way to build a query from an array.

So, instead of something like this:

$post = Post::isPublished()
    ->where('id', $id)
    ->firstOrFail();

I create an array ['isPublished', 'id' => $id] that is passed to a function defined in a trait.

protected function buildQuery(array $attributes)
{
    if (count($attributes) == 0) {
        app()->abort(404);
    }
    
    $query = (new $this->model);

    foreach ($attributes as $key => $value) {
        $method_name = camel_case(!is_numeric($key) ? $key : $value);
        $scope_method_name = camel_case('scope'.$method_name);

        // Call a local scope method on the model or resume 
        // with normal where equals clause.
        if (method_exists($this->model, $scope_method_name)) {
            $query = call_user_func([$query, $method_name], $value);
        } else {
            $query = $query->where($key, $value);
        }
    }

    return $query;
}

This then allows me to call local scopes and also have a default functionality of $query->where('column', $value) if not using a local scope. The resulting call could look something like...

$attributes = [
    'isPublished', 
    'id' => $id
];

$post = $this->posts
    ->buildQuery($attributes)
    ->firstOrFail();

At the end of the day, an ORM is your friend. So in this case, I still end up using Eloquent over this.