laravel scout search with multiple model - php

I used this old code, and start refactor it with use of laravel/scout.
This code is a simply search function thet search in posts and pages too, and show result mixed paginate.
Old code:
public function search($term) {
$post = Post::where('title', 'LIKE', $term);
$page = Page::where('content', 'LIKE', $term);
$result = $page->union($post)->orderBy('created_at')->paginate();
return $result;
}
New Code not working:
public function search($term) {
$post = Post::search($term);
$page = Page::search($term);
$result = $page->union($post)->orderBy('created_at')->paginate();
return $result;
}
get error: Method Laravel\Scout\Builder::union does not exist.
What is the best syntax for this problem?

Since Scout\Builder doesn't support union. And it would be non trivial to implement union functionality for all possible search engines supported by Scout.
However, Scout\Builder provides a query() function to customise the eloquent results query.
This provides kind of an escape hatch, where by scout can be leveraged on one model (out of the two)
public function search($term)
{
$postQuery = Post::query()
->where('some_column', 'like', $term . "%");
$results = Page::search($term)
->query(
fn($query) => $query->union($postQuery)
->orderBy('created_at', 'desc')
)
->paginate();
}
Laravel Scout - Customizing the Eloquent Results Query
For Algolia Engine
If using Algolia as search engine, there's Algolia Scout Extended which supports search with multiple models.
For other Engines
Another approach to search multiple models with Scout and get paginated results would be:
Get search results for individual models using Scout (using Model::search('query')->get();)
Concat the resulting collections
Paginate the collection
Define a macro for Illuminate\Support\Collection in the boot method of a Service Provider (for eg in App\Providers\AppServiceProvider)
<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Collection::macro('paginate', function ($perPage, $total = null, $page = null, $pageName = 'page') {
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$total ? $this : $this->forPage($page, $perPage)->values(),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
});
}
}
Then in the controller
public function search($term) {
$post = Post::where('title', 'LIKE', $term)->get();
$page = Page::where('content', 'LIKE', $term)->get();
$result = $page->concat($post)->sortBy('created_at')->paginate(5);
return $result;
}

Related

Mock Laravel Model with Database query using Eloquent Builder

I would like to mock the model TemplateLibraryCategory so that I don't have to hit the database on my unit test. Whenever I run the test I get the following:
There was 1 error:
1) Tests\Unit\Http\Filters\V2\Template\TemplateLibraryFilterTest::filter_is_applied_when_category_query_is_passed_with_value
Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\Checklist\TemplateLibraryCategory].
Which gives me the impression that the model is not being mocked as it should be.
This is how I'm mocking the model TemplateLibraryCategory
/**
* #test
* #group filters
* #group template-library
* #group filter-template-library
*/
public function filter_is_applied_when_category_query_is_passed_with_value()
{
$mock = Mockery::mock(TemplateLibraryCategory::class);
$mockBuilder = Mockery::mock(Builder::class);
$templateCategory = $this->createPartialMock(TemplateLibraryCategory::class, ['withSubcategories']);
$mockBuilder->shouldReceive('firstOrFail')->once()->andReturn($templateCategory);
$mock->shouldReceive('where')->once()->andReturn($mock);
$this->request->expects($this->any())->method('query')->willReturn(['category' => 'test-slug']);
$this->SUT = new TemplateFilter($this->request);
$checklistQuery = TemplateLibraryTemplate::filter($this->SUT);
$this->assertStringContainsString('`template_id` and `id` in', $checklistQuery->toSql());
}
Then on my TemplateFilter I have the following:
public function category(string $slug): void
{
$category = TemplateLibraryCategory::where('slug', $slug)->firstOrFail();
$this
->builder
->whereHas('categories', function ($query) use ($category) {
$categories = $category->withSubcategories()->pluck('id');
$query->whereIn('id', $categories);
});
}
By now I would guess that TemplateLibraryCategory is being mocked and both the method where and firstOrFail is mocked as well. Why is the test hitting the database?

Dynamic query eloquent

I have these 2 linked models: jobs and job_translations. A job have many translations. So in my job model, there is :
/**
* Get the translations for the job.
*/
public function translations()
{
return $this->hasMany('App\Models\JobTranslation');
}
In my controller, I want to build a query dynamically like that :
$query = Job::query();
if ($request->has('translation')) {
$query->translations()->where('external_translation', 'ilike', '%'.$request->translation.'%');
}
$jobs = $query->paginate(10);
I have this error :
Call to undefined method Illuminate\Database\Eloquent\Builder::translations()
Is it possible to do such a dynamic query with Eloquent?
Yes, it is possible. What you are looking for is whereHas('translations', $callback) instead of translations():
$query = Job::query();
if ($request->has('translation')) {
$query->whereHas('translations', function ($query) use ($request) {
$query->where('external_translation', 'ilike', '%'.$request->translation.'%');
});
}
$jobs = $query->paginate(10);
Your query can be improved further by using when($condition, $callback) instead of an if:
$jobs = Job::query()
->when($request->translation, function ($query, $translation) {
$query->whereHas('translations', function ($query) use ($translation) {
$query->where('external_translation', 'ilike', "%{$translation}%");
});
})
->paginate(10);
the issue you should do eager loading to detected on next chained query like this:
$query = Job::with('translations')->query();

Filtering Laravel resource collections is there a better way

I am experimenting with creating a Restful API in Laravel and I am making use of the resources feature designed for API output. I have created the following models:
Book, Author and Category. I also created a resource for each of these models.
There is a one to many relationship between author and book and a many to many between category and book which has a pivot table.
I can return a collection of all books easily with the following:
return BookResource::collection(Book::with(['author', 'categories'])->paginate(10));
But I want to easily filter by author and category so I have implemented it in the following way inside my controller:
public function index(request $request)
{
//if no filer params are passed in return all books with author and categories
if (!$request->has('author') && !$request->has('category')) {
return BookResource::collection(Book::with(['author', 'categories'])->paginate(10));
}
//author param is passed in
if($request->has('author') && !$request->has('category')){
$authorName = $request->author;
return BookResource::collection(Book::whereHas('author', function ($query) use ($authorName) {
$query->where('name', $authorName);
})->get());
}
//category param is passed in
if(!$request->has('author') && $request->has('category')){
$categoryName = $request->category;
return BookResource::collection(Book::whereHas('categories', function ($query) use ($categoryName) {
$query->where('name', $categoryName);
})->get());
}
}
Is there a better more efficient way of returning the BookResource collection filtered by author and category?
Please try to implement this way. Hope this helps. Thanks.
public function index(){
$author = request ('author', null);
$category = request ('category', null);
$books = Book::with(['author', 'categories'])->when($author, function ($query) use ($author) {
return $query->whereHas('author', function ($query) use ($author){
$query->where('name', $author);
});
})->when($category, function ($query) use ($category) {
return $query->whereHas('categories', function ($query) use ($category) {
$query->where('name', $category);
});
})->paginate(10);
return BookResource::collection($books);
}

Laravel where on relationship object

I'm developing a web API with Laravel 5.0 but I'm not sure about a specific query I'm trying to build.
My classes are as follows:
class Event extends Model {
protected $table = 'events';
public $timestamps = false;
public function participants()
{
return $this->hasMany('App\Participant', 'IDEvent', 'ID');
}
public function owner()
{
return $this->hasOne('App\User', 'ID', 'IDOwner');
}
}
and
class Participant extends Model {
protected $table = 'participants';
public $timestamps = false;
public function user()
{
return $this->belongTo('App\User', 'IDUser', 'ID');
}
public function event()
{
return $this->belongTo('App\Event', 'IDEvent', 'ID');
}
}
Now, I want to get all the events with a specific participant.
I tried with:
Event::with('participants')->where('IDUser', 1)->get();
but the where condition is applied on the Event and not on its Participants. The following gives me an exception:
Participant::where('IDUser', 1)->event()->get();
I know that I can write this:
$list = Participant::where('IDUser', 1)->get();
for($item in $list) {
$event = $item->event;
// ... other code ...
}
but it doesn't seem very efficient to send so many queries to the server.
What is the best way to perform a where through a model relationship using Laravel 5 and Eloquent?
The correct syntax to do this on your relations is:
Event::whereHas('participants', function ($query) {
return $query->where('IDUser', '=', 1);
})->get();
This will return Events where Participants have a user ID of 1. If the Participant doesn't have a user ID of 1, the Event will NOT be returned.
Read more at https://laravel.com/docs/5.8/eloquent-relationships#eager-loading
#Cermbo's answer is not related to this question. In that answer, Laravel will give you all Events if each Event has 'participants' with IdUser of 1.
But if you want to get all Events with all 'participants' provided that all 'participants' have a IdUser of 1, then you should do something like this :
Event::with(["participants" => function($q){
$q->where('participants.IdUser', '=', 1);
}])
N.B:
In where use your table name, not Model name.
for laravel 8.57+
Event::whereRelation('participants', 'IDUser', '=', 1)->get();
With multiple joins, use something like this code:
$userId = 44;
Event::with(["owner", "participants" => function($q) use($userId ){
$q->where('participants.IdUser', '=', 1);
//$q->where('some other field', $userId );
}])
Use this code:
return Deal::with(["redeem" => function($q){
$q->where('user_id', '=', 1);
}])->get();
for laravel 8 use this instead
Event::whereHas('participants', function ($query) {
$query->where('user_id', '=', 1);
})->get();
this will return events that only with partcipats with user id 1 with that event relastionship,
I created a custom query scope in BaseModel (my all models extends this class):
/**
* Add a relationship exists condition (BelongsTo).
*
* #param Builder $query
* #param string|Model $relation Relation string name or you can try pass directly model and method will try guess relationship
* #param mixed $modelOrKey
* #return Builder|static
*/
public function scopeWhereHasRelated(Builder $query, $relation, $modelOrKey = null)
{
if ($relation instanceof Model && $modelOrKey === null) {
$modelOrKey = $relation;
$relation = Str::camel(class_basename($relation));
}
return $query->whereHas($relation, static function (Builder $query) use ($modelOrKey) {
return $query->whereKey($modelOrKey instanceof Model ? $modelOrKey->getKey() : $modelOrKey);
});
}
You can use it in many contexts for example:
Event::whereHasRelated('participants', 1)->isNotEmpty(); // where has participant with id = 1
Furthermore, you can try to omit relationship name and pass just model:
$participant = Participant::find(1);
Event::whereHasRelated($participant)->first(); // guess relationship based on class name and get id from model instance
[OOT]
A bit OOT, but this question is the most closest topic with my question.
Here is an example if you want to show Event where ALL participant meet certain requirement. Let's say, event where ALL the participant has fully paid. So, it WILL NOT return events which having one or more participants that haven't fully paid .
Simply use the whereDoesntHave of the others 2 statuses.
Let's say the statuses are haven't paid at all [eq:1], paid some of it [eq:2], and fully paid [eq:3]
Event::whereDoesntHave('participants', function ($query) {
return $query->whereRaw('payment = 1 or payment = 2');
})->get();
Tested on Laravel 5.8 - 7.x

How to use KNPPaginatorBundle to paginate results using Doctrine Repository?

I'm working on a Symfony2 project and I decided to use KNPPaginatorBundle to build an easy pagination system. So I created a Product entity and I want to add the paginator to indexAction action (generated by CRUD command).
// Retrieving products.
$em = $this->getDoctrine()->getManager();
//$entities = $em->getRepository('LiveDataShopBundle:Product')->findAll();
$dql = "SELECT a FROM LiveDataShopBundle:Product a";
$entities = $em->createQuery($dql);
// Creating pagnination
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$entities,
$this->get('request')->query->get('page', 1),
20
);
It works fine but I want to use the Product's repository instead of creating the query directly in the controller. How can I do that ?
In fact, directly add the collection of results to the paginate object is just too slow because its load all products then paginate the ArrayCollection.
Thanks in advance.
K4
I suggest using QueryBuilder in your ProductRepository and then passing that to the paginator:
ProductRepository extends EntityRepository
{
// This will return a QueryBuilder instance
public function findAll()
{
return $this->createQueryBuilder("p");
}
}
In the controller:
$products = $productRepository->findAll();
// Creating pagnination
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$products,
$this->get('request')->query->get('page', 1),
20
);
I think in some cases we could use Closure and pass to it a QueryBuilder object.
In your ProductRepository you could do something like this:
ProductRepository extends EntityRepository
{
public function findAllPublished(callable $func = null)
{
$qb = $this->createQueryBuilder('p');
$qb->where('p.published = 1');
if (is_callable($func)) {
return $func($qb);
}
return $qb->getQuery()->getResult();
}
}
and then in ProductController:
public function indexAction(Request $request)
{
$em = $this->get('doctrine.orm.entity_manager');
$paginator = $this->get('knp_paginator');
$func = function (QueryBuilder $qb) use ($paginator, $request) {
return $paginator->paginate($qb, $request->query->getInt('page', 1), 10);
};
$pagination = $em->getRepository('AppBundle:Report')->findAllPublished($func);
// ...
}
I think it more flexible and you could use findAllPublished method to get both paginated or NOT paginated results if you need.
Also keep in mind that callable type hint work in PHP >=5.4! Please, check docs for more info.
In our project we want to avoid using Doctrine queries in controllers. We have also separate layers. Controllers must not access the database. So I included pagination in the Repository.
Here my code in controller:
public function indexAction(Request $request)
{
$userRepository = $this->get('user_repository');
$page = intval($request->query->get('page', 1));
$pages = 0;
$users = $userRepository->findAllPaginated($pages, $page - 1, 10);
return $this->render('User:index.html.twig', array(
'users' => $users,
'page' => $page,
'pages' => $pages,
));
}
And here is the important code in my repository:
use Doctrine\ORM\Tools\Pagination\Paginator;
class UserRepository extends EntityRepository
{
/**
* #return User[]
*/
public function findAllPaginated(&$pages, $startPage = 0, $resultsPerPage = 5)
{
$dql = 'SELECT u FROM CoreBundle:User u';
$query = $this->getEntityManager()->createQuery($dql)
->setFirstResult($startPage * $resultsPerPage)
->setMaxResults($resultsPerPage);
$paginator = new Paginator($query);
$count = $paginator->count();
$pages = floor($count/$resultsPerPage);
return $paginator; // on $paginator you can use "foreach", so we can say return value is an array of User
}
}

Categories