Laravel Scout map in toSearchableArray relationship fields - laravel

Is it possible to map in toSearchableArray relationship fields as well. What I mean, having User model I try to search in related model fields as well like;
/**
* Get the indexable data array for the model.
*
* #return array
*/
public function toSearchableArray()
{
return [
'name' => $this->name,
'plz' => $this->account->zip,
];
}
/**
* #return HasOne
*/
public function account(): HasOne
{
return $this->hasOne(Member::class);
}
In controller searching after results
if($request->has('s')) {
$founds = User::search($request->get('s'))->get();
}
will throw Attempt to read property "zip" on null
I do not really find any infos in documentation related to this question

I do have two ways to do it of which I consider one of them as crude.
Method 1:
Here's an example implementation where you're searching all of one model and then a relationship (accounts)
public function toSearchableArray()
{
$array = $this->toArray();
$array = $this->transform($array);
$array['country'] = $this->countries->map(function ($data) {
return $data['name'] ?? '';
})->toArray();
return $array;
}
Method 2:
public function toSearchableArray()
{
$array = $this->toArray();
// Customize array...
$array = [
'user_name' => $this->user_name,
'country' => $this->getCountryNameById($this->country_id),
...
];
return $array;
}
where relationship is defined in helpers or you can make a separate trait and import it in model with method.
Here relationship for country is defined within the model as
//Get Country name By id
function getCountryNameById($id) {
$country = \App\Country::select('name')->find($id);
return $country->name ?? '';
}

Related

Laravel Nova Actions BelongsTo field not working

I have this simple action:
/**
* Perform the action on the given models.
*
* #param \Laravel\Nova\Fields\ActionFields $fields
* #param \Illuminate\Support\Collection $models
* #return mixed
*/
public function handle(ActionFields $fields, Collection $models)
{
foreach ($models as $model) {
$model->update([
'user_id' => $fields->user
]);
}
}
/**
* Get the fields available on the action.
*
* #return array
*/
public function fields()
{
return [
BelongsTo::make('User', 'user', User::class),
];
}
At first, it seems fine, but when I select User from BelongsTo relation and try to save exception is throwing:
Argument 1 passed to Laravel\Nova\Fields\BelongsTo::getRelationForeignKeyName() must be an instance of Illuminate\Database\Eloquent\Relations\Relation, instance of Illuminate\Support\Fluent given, called in /Users/rd/Sites/bns-crm/vendor/laravel/nova/src/Fields/BelongsTo.php on line 212
Yes i know i'm late but - here's a solution for this:
Use a Select-Field instead of BelongsTo and Pluck your options to build Key-Value pairs:
public function fields()
{
return [
Select::make('debitor')->options(\App\Models\Debitor::pluck('Name', 'id'))
];
}
Then in the handle you should geht the ids in $fields:
public function handle(ActionFields $fields, Collection $models) {
Log::info($fields);
}
Maybe I'm late, but, for the ones like me wanting to use the BelongsTo searchable field because the model they want to search in contains too much records to pack them in a normal Select field here is the solution I found:
Create a class in App\Nova\Fields with this code:
<?php
namespace App\Nova\Fields;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Http\Requests\NovaRequest;
class BelongsToForActions extends BelongsTo
{
public function fillForAction(NovaRequest $request, $model)
{
$attribute = $this->attribute;
if ($request->exists($attribute)) {
$value = $request[ $attribute ];
$model->{$attribute} = $this->isNullValue($value) ? null : $value;
}
}
}
Then use it like you would use a normal BelongsTo field. Just remember to fill the 3 arguments on the make, so, for example:
BelongsToForActions::make('User', 'relation', \App\Nova\User::class)->searchable()
Remember that 'relation' must exist.
Check your namespaces. Did you imported right class? User class must be resource class
public function fields()
{
return [
BelongsTo::make('User', 'user', User::class),
];
}
I actually fixed this by mocking the key value pair used in this relationship.
First I build an array with the ID column as key and the name column as value.
$clients = Client::all()
->keyBy('id')
->map(fn($client): string => $client['name'])
->toArray();
Then I use the Select nova field to display it.
Select::make('Klant', 'client')
->searchable()
->options($clients)
->rules('required'),

Using HasMany relationship when Model and table have different names

Like the title says, my Field model and application_fields table have different names. I already set the table name on the Field model to the correct table name. A set the I have another model, Application, and its controller, ApplicationController. In the index method of the ApplicationController, I am trying to retrieve the application and its fields. The Field model has a belongsTo relationship to Application, and the Application model has a hasMany relationship to Field.
The code seems to work, except it pulls the values for the rows from the applications table. So if the applications table has the following rows:
1 'AppOne' 'An App'
2 'AppTwo' 'Another App'
3 'AppThree' 'A third App'
AppOne, AppTwo, and AppThree are returned as the values for the fields.
Can someone give me some advice?
Here is the code for them( I only copied what was related to the issue, not the entire file):
Application model:
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'application_fields';
public function fields()
{
return $this->hasMany('App\Field');
}
Field model:
public function application()
{
return $this->belongsTo('App\Application');
}
ApplicationController:
/**
* Display the dashboard for a single application.
*
* #param String $id
* #return View
*/
public function show( $id )
{
$app = Application::where('id', $id)->first();
$fields = $app::with('application_fields')->get();
return view('applications.show', [
'application' => $app,
'fields' => $fields,
]);
}
View( I stripped out the HTML):
{{ $application->name }}
{{ $application->description }}
#foreach($fields as $field)
{{ $field->name}}
#endforeach
I would appreciate any advice on what I am doing wrong. Thanks
your show method is wrong, eager loading does not work that way :)
public function show( $id ) {
$app = Application::with('fields')->find($id);
$fields = $app->fields;
return view('applications.show', [
'application' => $app,
'fields' => $fields,
]);
}
you can also use model Binding if your route is like that : applications/{app}
public function show(Application $app) {
$fields = $app->fields;
return view('applications.show', [
'application' => $app,
'fields' => $fields,
]);
}

Create new Post with default Category belongsToMany

I have a Post/Category manyToMany relations and would like to be able to attach a default category named "Uncategorised" to each new post that is created. How can I do that? A BelongsToMany method only works on the Details page, not on Create page.
BelongsToMany::make(__('Categories'), 'categories', Category::class),
You can also set default value to your database field so that you can omit passing category and will be taken default to Uncategorised like if you are using MySQL you can do it this way by creating migration
$table->text('category')->default(0);
Because the BelongsToMany not show on mode create in Post Nova model. So we have to make our custom Select, by add this code to your fields:
public function fields(Request $request)
{
if($request->editMode=="create"){
$categories = \App\Category::get(['id','name']);
$options = [];
foreach($categories as $value){
$options[$value->id] = $value->name;
}
return [
ID::make()->sortable(),
Text::make('Title'),
Text::make('Summary'),
Textarea::make('Content'),
Select::make('Categories', 'category_id')
->options($options)
->displayUsingLabels()
->withMeta(['value' => 1]) // 1 = id of Uncategorised in categories table
];
}
return [
ID::make()->sortable(),
Text::make('Title'),
Text::make('Summary'),
Textarea::make('Content'),
BelongsToMany::make('Categories','categories')->display('name'),
];
}
Don’t forget relationship function in both, Post and Category model:
class Post extends Model
{
public function categories(){
return $this->belongsToMany(Category::class, 'category_post', 'post_id', 'category_id');
}
}
And:
class Category extends Model
{
public function posts(){
return $this->belongsToMany(Post::class,'category_post', 'category_id', 'post_id');
}
}
Then, custom the function process the data on mode Create of Post resource page, it’s at nova\src\Http\Controllers\ResourceStoreController.php, change function handle to this:
public function handle(CreateResourceRequest $request)
{
$resource = $request->resource();
$resource::authorizeToCreate($request);
$resource::validateForCreation($request);
$model = DB::transaction(function () use ($request, $resource) {
[$model, $callbacks] = $resource::fill(
$request, $resource::newModel()
);
if ($request->viaRelationship()) {
$request->findParentModelOrFail()
->{$request->viaRelationship}()
->save($model);
} else {
$model->save();
// your code to save to pivot category_post here
if(isset($request->category_id)&&($resource=='App\Nova\Post')){
$category_id = $request->category_id;
$post_id = $model->id;
\App\Post::find($post_id)->categories()->attach($category_id);
}
}
ActionEvent::forResourceCreate($request->user(), $model)->save();
collect($callbacks)->each->__invoke();
return $model;
});
return response()->json([
'id' => $model->getKey(),
'resource' => $model->attributesToArray(),
'redirect' => $resource::redirectAfterCreate($request, $request->newResourceWith($model)),
], 201);
}
}
All runs well on my computer. A fun question with me! Hope best to you, and ask me if you need!
What I ended up doing was saving the data on Post Model in boot().
public static function boot()
{
parent::boot();
static::created(function (Post $post) {
$post->categories()->attach([1]);
});
}

Laravel many to many relationship with uuid returns always empty

I use Laravel 5.8 and changed my model's autoincrement id to uuid. Since then I have some trouble with my many-to-many relationship that was defined between 2 of my models Event and User (with pivot table events_users).
The problem :
Now when I request all element that join both table (I have 2 records in my pivot table) I always get an empty array back.
When debugging the sql, I see that the where clause param is not set :
// Generated sql
select `users`.*, `events_users`.`event_id` as `pivot_event_id`, `events_users`.`user_id` as `pivot_user_id`, `events_users`.`created_at` as `pivot_created_at`, `events_users`.`updated_at` as `pivot_updated_at`
from `users`
inner join `events_users` on `users`.`id` = `events_users`.`user_id`
where `events_users`.`event_id` = ?
// Bindings :
Array
(
[0] =>
)
Has someone any clue what I'm missing here ?
Here are the definition of my models :
class Event extends Model
{
protected $primaryKey = 'id';
protected $keyType = 'string';
public $incrementing = false;
// here some other model methods, fillable property, etc.
public function users()
{
return $this
->belongsToMany(User::class, 'events_users', 'event_id', 'user_id')
->withTimestamps();
}
}
Same declaration for User model, but with relation
public function events()
{
return $this
->belongsToMany(Event::class, 'events_users', 'user_id', 'event_id')
->withPivot(['created_at', 'updated_at']);
}
Then I retrieve the relations from the service with :
public function getSubscriptions($eventId)
{
$eventId = 'a1b7c5d6-8f86-44f4-f31a-46e32917d5c0'; // for debug purpose only
$event = Event::find($eventId);
foreach ($event->users as $user) {
print_r($user); die; // It never loops here as its length is 0 but should be 2...
}
\DB::listen(function ($query) {
print_r($query->sql);
print_r($query->bindings);
// $query->time
});
$subscriptions = $event
->users()
->get();
die;
return $subscriptions;
}
My DB contains the records
The problem was about another declaration in my models where I list the property.
I've initialized an id property there, which is probably in conflict with the uuid type or I don't know exactly what cause this drama...
Anyway, removing this line let the app work correctly.
/**
* #var array
* Rules used for fields validation
*/
public $rules = array(
'title' => 'required|string|max:255',
'start_date' => 'required|date|date_format:Y-m-d',
'end_date' => 'required|date|date_format:Y-m-d|after_or_equal:start_date',
'location' => 'string|max:254',
'latitude' => 'numeric',
'longitude' => 'numeric'
);
public $id = ""; // This is the line that create the bug... Remove it and it works !
public $title = "";
public $start_date = "";
public $end_date = "";
public $location = "";
public $latitude = "";
public $longitude = "";

Laravel json response returns encrypted data

I'm using an Encryptable trait to encrypt my data for the a Room model.
RoomController (/rooms) returns the decrypted data but ApiRoomController (/api/rooms) does not.
How could I make it returns the decrypted data?
Encryptable Trait
trait Encryptable
{
public function getAttribute($key)
{
$value = parent::getAttribute($key);
if (in_array($key, $this->encryptable) && $value !== '' && $value !== null ) {
$value = Crypt::decrypt($value);
}
return $value;
}
public function setAttribute($key, $value)
{
if (in_array($key, $this->encryptable)) {
$value = Crypt::encrypt($value);
}
return parent::setAttribute($key, $value);
}
}
RoomController index function
public function index()
{
$rooms = Room::select('id', 'name')->get()->sortBy('name')->values()->all();
return view('rooms.index')->withRooms($rooms);
}
ApiRoomController index function
public function index()
{
$rooms = Room::select('id', 'name')->get()->sortBy('name')->values()->all();
return response()->json($rooms);
}
I found a way using API Resources:
php artisan make:resource Rooms --collection
Then in your app/Http/Resources/Rooms.php file:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
// more fields here
];
}
Then in your ApiRoomController.php file:
use App\Http\Resources\Rooms;
public function index()
{
$rooms = Room::select('id', 'name')->get()->sortBy('name')->values()->all();
return Rooms::collection($rooms);
}
Seems like #emotality came up with a good solution for this already...
However, the reason for this not working as you expected is because the underlying Model's toArray() / toJson() methods do not call the getAttribute() method in your trait.
This is important because the response()->json() method maps the given collection and calls the toJson() method on each model in order to prepare it for a response.
Therefore, you can also solve this by overwriting the toArray method in your model.
class Room extends Model
{
use Encryptable;
public function toArray()
{
return [
'id' => $this->id,
'name' => $this->name,
// ...
];
}
}

Resources