Relational search using laravel-scout-tntsearch-driver package for Laravel Scout - php

I'm using the laravel-scout-tntsearch-driver package for Laravel Scout. I have implemented it, and everything works fine. But now I want to do a relational search. I have cities that have many companies.
City.php
public function companies()
{
return $this->hasMany(Company::class);
}
Company.php
public function city()
{
return $this->belongsTo(City::class);
}
public function toSearchableArray()
{
return [
'id' => $this->id,
'title' => $this->title
];
}
Now the search is working only for all companies.
Company::search('bugs bunny')->get();
Also where clauses don't work here. I want something like this:
Route::get('/search/{city}', function (\App\City $city) {
$companies = $city->companies()->search('bugs bunny');
});
I think you got the idea. Thanks!

First, I would move the logic to a controller. After doing that, you could have a method in the controller that implements the required search like this:
public function search(City $city){
$companiesInCity = Company::where('city_id', $city->id)->get('id')->toArray();
$companiesMatching = Company::search('bugs bunny')->whereIn('id', $companiesInCity)->get();
return view('search.result', [
'result' => $result
]);
}
And finally call the function from your routeing web.php.
Working in my dev env.
Hope this helps!

Related

Laravel - Shared data reduce number of queries

I am trying to make a global search feature on my website, and for that I need to query the database to get the records. The frontend of my application expect the following array:
[
['name' => 'Result #1...', 'url' => 'http://...'],
['name' => 'Result #2...', 'url' => 'http://...'],
['name' => 'Result #3...', 'url' => 'http://...'],
]
To accomplish this, I have added the below in my AppServiceProvider:
public function boot()
{
view()->composer('*', function($view) use ($auth) {
return \View::share('SearchData', (new GlobalSearch($auth->user()->currentTeam))->all());
});
}
The $SearchData is created in the GlobalSearch class:
class GlobalSearch
{
public $data;
public function __construct(public Team $team){
$this->data = $team->properties()->with(['leases', 'leases.tenant', 'leases.files', 'leases.invoices']);
}
protected function propertyData() : array
{
$properties = $this->data->get();
return $properties->map(function ($property) {
$array['name'] = \Str::limit($property->address, 40);
$array['url'] = route('properties.show', ['property' => $property]);
return $array;
})->toArray();
}
public function all() : array
{
return $this->propertyData();
}
}
Now the above code does work - I successfully get an array in the correct mapping. However, in my database I only have 1 property in the properties table - yet, there are being executed 90 duplicate queries for a single page load.
Why is this happening? I can't seem to locate why these queries are being duplicated
You can actually remove the view()->composer('*', function) part. Since View::share() does the exact same.
$searchData = (new GlobalSearch($auth->user()->currentTeam))->all();
View::share('SearchData', $searchData);
Optionally:
You could bind the GlobalSearch class to the container in your AppServiceProvider. Which will make the class available via app(GlobalSearch::class) wherever you want in the application. This means the query will only run once during the initialization process and maintain the data.
More info about singleton bindings: https://laravel.com/docs/8.x/container#binding-a-singleton
$this->app->singleton(GlobalSearch::class, function ($app) {
return new GlobalSearch(auth()->user()->team);
});

Create Model with HasMany relation using Repository Pattern

I'm trying to implement the repository pattern and save a relationship using the create method as shown below.
abstract class EloquentRepository implements Repository {
public function create($data)
{
return $this->model->create($data);
}
}
Within my controller I have injected the repository:
public function __construct(SubscriberRepository $subscriberRepository,
SubscribableRepository $subscribableRepository)
{
$this->subscriberRepository = $subscriberRepository;
$this->subscribableRepository = $subscribableRepository;
}
My store method looks like:
public function store(CreateSubscriberRequest $request): JsonResponse
{
$subscribable = $this->subscribableRepository->findByIdentifier($request->input('type'))
->firstOrFail();
$attributes = [
'name' => $request->input('name'),
'email' => $request->input('email')
];
$subscriber = $this->subscriberRepository->create($attributes);
}
Subscriber Model
public function subscribable()
{
return $this->belongsTo(Subscribable::class, 'subscribable_id');
}
Subscribable Model
public function subscribers()
{
return $this->hasMany(Subscriber::class);
}
My issue General error: 1364 Field 'subscribable_id' doesn't have a default value is because the subscribable_id is a foreign key and not set in the create method.
How do I relate the subscribable model, setting the subscribable_id? I don't think setting the subscribable_id in the fillable property is the way to go with this.
Many thanks in advance.
Laravel gives functionality to save relations using the related model instances.
So You can save relation by calling create method on relation like this:
public function store(CreateSubscriberRequest $request): JsonResponse
{
$subscribable = $this->subscribableRepository->findByIdentifier($request->input('type'))
->firstOrFail();
$attributes = [
'name' => $request->input('name'),
'email' => $request->input('email')
];
$subscribable->subscribers()->create($attributes);
}
See laravel doc on relationship

Laravel hasMany save

I've two tables to save data to. One of them has foreign key so that I have one-to-many relationship. However, I don't understand how to save data into two table simultaneously. I have one query which contains data for one table and for another that should be attached to first one.
That is the main model
class Site extends Model
{
protected $fillable = ['path', 'site_link'];
public $timestamps = false;
public function features() {
return $this->hasMany('App\SiteFeature');
}
}
And this is the sub-model
class SiteFeature extends Model
{
protected $fillable = ['feature', 'site_id'];
public $timestamps = false;
}
Right now my controller looks like this
class SiteController extends BaseController
{
public function index()
{
return Site::all();
}
public function show(Site $id)
{
return $this->response->item($id, new SiteTransformer);
}
public function store(Request $request)
{
$site = Site::create($request->all());
return response()->json($site, 201);
}
I know that it would save it as one piece of data. And I ask you for help me to split data into two tables. In docs I've found the way to store with relationship to an existing model in DB, however I don't have that model at the moment of creation.
Solved that way
public function store(Request $request)
{
$site = Site::create([
"path" => $request->path,
"site_link" => $request->link,
]);
foreach ($request->features as $feature) {
$site->features()->save(new SiteFeature(["feature" => $feature]));
}
return response()->json($site, 201);
}
There are certain things you have to make sure of.
First: In your SiteFeature-Model the inverse relation to the Site-Models seems to be missing.
There should be a function like:
public function site()
{
return $this->belongsTo('App\Site');
}
See Laravel 5.6 - Eloquent Relationships, One-to-Many for this.
If however you have a relationship where (n) Sites can be related to (n) SiteFeatures, your relations and inverse relations have to be different.
(And there will also have to be a pivot table, in which you can store the n-to-n relation)
See Laravel 5.6 - Eloquent Relationships, Many-to-Many in that case.
Since your Question does not describe what is received with $request, here's what you should consider:
Validate your inputs. This will make sure you don't save garbage to your database
Check if you already have some part of the data-set you want to save, then save in two steps:
First step:
$site = Site::firstOrCreate(['path' => $request['input_name_for_path'],
'site_link' => $request['input_name_for_site_link'],
]);
This will give you a proper Site-Model saved to the database.
(Note, that this shows how you manually assign values to the fillable fields defined in the model in case you have different input field names)
Now you can go on an save the SiteFeature-Model connected to it:
$feature = SiteFeature::firstOrCreate('feature' => $request['input_name_for_feature');
$site->features()->attach($feature->id);
This should do the trick saving both, a new (or old) Site and a related SiteFeature to your database.
If I misunderstood the question, feel free to add information and I will update.
It's the correct way to save data using hasMany relationship without creating a new object of lookup model.
// inside controller
public function store(Request $request)
{
$student = Student::create([
"name" => $request->get('name'),
"email" => $request->get('email'),
]);
foreach ($request->subjects as $subject) {
$student->subjects()->create(["title" => $subject['title']);
}
return response()->json($student, 201);
}
// inside User model
public function subjects()
{
return $this->hasMany('App\Models\Subject');
}

Laravel eager load resource collection

How can I eager load a resource collection relationship? I've made a resource which calls gravel_pits relationship
class GravelTypeResource extends Resource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'deleted_at' => $this->deleted_at,
'gravel_pits' => $this->gravel_pits,
];
}
}
On the model M:M relationship:
public function gravel_pits()
{
return $this->belongsToMany('App\GravelPit');
}
And from the API I am getting it back like this:
public function index()
{
return GravelTypeResource::collection(GravelType::all());
}
I can eager load it by doing
public function index()
{
return GravelTypeResource::collection(GravelType::with('gravel_pits'));
}
which works...but I can't control then what properties of gravel pits I actually want back, instead, eager load fetches them all. Is there a simple workaround to this?
you can use Resource Collections
GravelTypeResourceCollection::make($collection);
and since you can use load and loadMissing on eloquent collections you can do this
class GravelTypeResourceCollection extends ResourceCollection
{
$collects = GravelTypeResource::class;
public function __construct($resource){
$resource->loadMissing(['gravel_pits']);
parent::__construct($resource);
}
}
You can pass in a select to get just the fields you want. Just make sure you get the fields that the relationship is based on:
return GravelTypeResource::collection(GravelType::with('gravel_pits'=>function($query) {
$query->select(['id', 'gravel_type_id', 'column3', 'column4']);
});

Selecting all images from user which is clicked

I've started to learn Laravel version 5.4 recently but I have some error that I can't find a answer so far. I'm trying to make user-friendly urls and to load images of user by username not by ID.
So far I've made this in my controller
public function author(User $author) {
$authorName = $author->username;
$image = $author->with('author')->latestFirst()->paginate($this->limit);
return view("author", compact('image', 'authorName'));
}
This in my route
Route::get('/author/{author}', [
'uses' => 'HomeController#author',
'as' => 'author'
]);
And this is my link
{{ $categoryImage->author->username }}
This is my Image model
public function author()
{
return $this->belongsTo(User::class, 'image_author');
}
And User model
public function images()
{
return $this->hasMany(Image::class);
}
Whatever link I click I always get this error
No query results for model [App\User].
I'm not even sure if my controller and routes are correct.
This should solve it. https://laravel.com/docs/5.4/routing#implicit-binding
Put this in your user model:
public function getRouteKeyName()
{
return 'username';
}

Categories