Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet committed Jul 8, 2020
1 parent 3dbb04c commit d65c67a
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 209 deletions.
21 changes: 7 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,15 @@ No configuration!
### Basic

```php
$results = Search::new()
->add(Post::class, 'title')
$results = Search::add(Post::class, 'title')
->add(Video::class, 'title')
->get('foo');
```

### Pagination

```php
$results = Search::new()
->add(Post::class, 'title')
$results = Search::add(Post::class, 'title')
->add(Video::class, 'title')

->paginate()
Expand All @@ -51,26 +49,23 @@ $results = Search::new()
### Scoped queries

```php
$results = Search::new()
->add(Post::published(), 'title')
$results = Search::add(Post::published(), 'title')
->add(Video::where('views', '>', 2500), 'title')
->get('foo');
```

### Multiple columns

```php
$results = Search::new()
->add(Post::class, ['title', 'body'])
$results = Search::add(Post::class, ['title', 'body'])
->add(Video::class, ['title', 'subtitle'])
->get('foo');
```

### Order results

```php
$results = Search::new()
->add(Post::class, 'title', 'publihed_at')
$results = Search::add(Post::class, 'title', 'publihed_at')
->add(Video::class, 'title', 'released_at')
->orderByDesc() // optional
->get('foo');
Expand All @@ -79,8 +74,7 @@ $results = Search::new()
### Add wildcard on left side of the term

```php
$results = Search::new()
->add(Post::class, 'title')
$results = Search::add(Post::class, 'title')
->add(Video::class, 'title')
->wildcardLeft()
->get('foo');
Expand All @@ -89,8 +83,7 @@ $results = Search::new()
### Eager load relations

```php
$results = Search::new()
->add(Post::with('comments'), 'title')
$results = Search::add(Post::with('comments'), 'title')
->add(Video::with('likes'), 'title')
->get('foo');
```
Expand Down
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,12 @@
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"ProtoneMedia\\LaravelCrossEloquentSearch\\ServiceProvider"
]
}
}
}
26 changes: 1 addition & 25 deletions src/PendingQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ public function getFreshBuilder(): Builder
return clone $this->builder;
}

public function newQueryWithoutScopes(): Builder
{
return $this->getFreshBuilder();
}

public function getQualifiedColumns(): Collection
{
return $this->columns->map(fn ($column) => $this->qualifyColumn($column));
Expand All @@ -63,33 +58,14 @@ public function getOrderByColumn(): string

public function qualifyColumn(string $column): string
{
if (Str::contains($column, '.')) {
return $column;
}

return $this->getQualifiedTable() . '.' . $column;
return $this->getModel()->qualifyColumn($column);
}

public function getQualifiedKeyName(): string
{
return $this->qualifyColumn($this->getModel()->getKeyName());
}

public function getTable(): string
{
return $this->getModel()->getTable();
}

public function getQualifiedTable(): string
{
return $this->key . '_' . $this->getModel()->getTable();
}

public function getTableAlias(): string
{
return $this->getTable() . ' as ' . $this->getQualifiedTable();
}

public function getQualifiedOrderByColumnName(): string
{
return $this->qualifyColumn($this->getOrderByColumn());
Expand Down
157 changes: 4 additions & 153 deletions src/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,161 +2,12 @@

namespace ProtoneMedia\LaravelCrossEloquentSearch;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Facade;

class Search
class Search extends Facade
{
private Collection $pendingQueries;
private int $perPage = 15;
private string $pageName = 'page';
private $page;
private string $orderByDirection;
private bool $wildcardLeft = false;
private Collection $terms;

public function __construct()
{
$this->pendingQueries = new Collection;

$this->orderByAsc();
}

public static function new(): self
{
return new static;
}

public function orderByAsc(): self
protected static function getFacadeAccessor()
{
$this->orderByDirection = 'asc';

return $this;
}

public function orderByDesc(): self
{
$this->orderByDirection = 'desc';

return $this;
}

public function add($query, $columns, string $orderByColumn = 'updated_at'): self
{
$pendingQuery = new PendingQuery(
is_string($query) ? $query::query() : $query,
Collection::wrap($columns),
$orderByColumn,
$this->pendingQueries->count()
);

$this->pendingQueries->push($pendingQuery);

return $this;
}

public function wildcardLeft(): self
{
$this->wildcardLeft = true;

return $this;
}

private function makeOrderBy(): string
{
$modelOrderKeys = $this->pendingQueries->map->getModelKey('order')->implode(',');

return "COALESCE({$modelOrderKeys})";
}

private function makeSelects(PendingQuery $currentPendingQuery): array
{
return $this->pendingQueries->flatMap(function (PendingQuery $pendingQuery) use ($currentPendingQuery) {
$qualifiedKeyName = $qualifiedOrderByColumnName = 'null';

if ($pendingQuery === $currentPendingQuery) {
$qualifiedKeyName = $pendingQuery->getQualifiedKeyName();
$qualifiedOrderByColumnName = $pendingQuery->getQualifiedOrderByColumnName();
}

return [
DB::raw("{$qualifiedKeyName} as {$pendingQuery->getModelKey()}"),
DB::raw("{$qualifiedOrderByColumnName} as {$pendingQuery->getModelKey('order')}"),
];
})->all();
}

public function paginate($perPage = 15, $pageName = 'page', $page = null): self
{
$this->page = $page ?: Paginator::resolveCurrentPage($pageName);
$this->pageName = $pageName;
$this->perPage = $perPage;

return $this;
}

public function addSearchQueryToBuilder(Builder $builder, PendingQuery $pendingQuery, string $term)
{
return $builder->where(function ($query) use ($pendingQuery) {
$pendingQuery->getQualifiedColumns()->each(
fn ($field) => $this->terms->each(
fn ($term) => $query->orWhere($field, 'like', ($this->wildcardLeft ? '%' : '') . "{$term}%")
)
);
});
}

private function buildQueries($term): Collection
{
return $this->pendingQueries->map(function (PendingQuery $pendingQuery) use ($term) {
return $pendingQuery->getFreshBuilder()
->select($this->makeSelects($pendingQuery))
->from(DB::raw($pendingQuery->getTableAlias()))
->tap(function ($builder) use ($pendingQuery, $term) {
$this->addSearchQueryToBuilder($builder, $pendingQuery, $term);
});
});
}

public function get($term)
{
$this->terms = Collection::make(str_getcsv($term, ' ', '"'))->filter();

if ($this->terms->isEmpty()) {
throw new EmptySearchQueryException;
}

$queries = $this->buildQueries($term);

$firstQuery = $queries->shift();

$queries->each(fn (Builder $query) => $firstQuery->union($query));
$firstQuery->orderBy(DB::raw($this->makeOrderBy()), $this->orderByDirection);

$results = $this->perPage
? $firstQuery->paginate($this->perPage, ['*'], $this->pageName, $this->page)
: $firstQuery->get();

$modelsPerType = $this->pendingQueries
->keyBy->getModelKey()
->map(function (PendingQuery $pendingQuery, $key) use ($results) {
$ids = $results->pluck($key)->filter();

return $ids->isNotEmpty()
? $pendingQuery->newQueryWithoutScopes()->whereKey($ids)->get()->keyBy->getKey()
: null;
});

return $results->map(function ($item) use ($modelsPerType) {
$modelKey = Arr::first(array_flip(array_filter($item->toArray())));

return $modelsPerType->get($modelKey)->get($item->$modelKey);
})
->pipe(fn (Collection $models) => new EloquentCollection($models))
->when($this->perPage, fn (EloquentCollection $models) => $results->setCollection($models));
return 'laravel-cross-eloquent-search';
}
}
26 changes: 26 additions & 0 deletions src/SearchFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types=1);

namespace ProtoneMedia\LaravelCrossEloquentSearch;

use Illuminate\Support\Traits\ForwardsCalls;

class SearchFactory
{
use ForwardsCalls;

/**
* Handle dynamic method calls into the a new Searcher.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->forwardCallTo(
new Searcher,
$method,
$parameters
);
}
}
Loading

0 comments on commit d65c67a

Please sign in to comment.