Check if key exists in eloquent model - php

I am trying to create a helper function that checks if a specific key exists inside an eloquent model to generate a dynamic query that only checks for the correct keys.
I have made to helper functions like such:
if(!function_exists('entityHasKey')){
function entityHasKey(Model $eloquentModel, string $key): bool
{
return array_key_exists($key, $eloquentModel->getAttributes());
}
}
if(!function_exists('hasEntityKeys')){
/**
* #param string $eloquentModel
* #param string[] $keys
* #return bool
*/
function entityHasKeys(string $eloquentModel, array $keys): bool
{
$hasKey = false;
foreach($keys as $key){
if(entityHasKey($eloquentModel, $key)){
$hasKey = true;
break;
}
}
return $hasKey;
}
}
In my head, the logic makes good sense, but the issue is now that I can't pass the attributes from the class (without having an instance). My best guess would be the use the static class instance, but that only gets me the name.
What is the best approach to check if a key exists on an eloquent model (without an instance necessarily)
usage example
(*other conditions * and entityHasKeys(MyCoolModel::class, $filters['columns']))

The Schema facade has a method called getColumnListing() which takes the table name as a parameter. If you want to get all the columns that are present in the table (which are essentially the possible attributes) you can do something like this.
You may use the app() helper to get the table name without an instance of the model.
use Illuminate\Support\Facades\Schema;
$attributes = Schema::getColumnListing(
app(\App\Models\User::class)->getTable()
);
You can then create a function that accepts an attribute name as a string or multiple attributes in an array that should be checked for, in combination with the model name as a string (e.g. User).
function modelHasAttributes(string $model, string|array $attributes): bool
{
$attributes = is_array($attributes) ? $attributes : [$attributes];
$modelAttributes = Schema::getColumnListing(
app("\App\Models\\$model")->getTable()
);
return count(array_intersect($attributes, $modelAttributes)) === count($attributes);
}
This will yield you the following results (tested with default User model).
modelHasAttributes('User', 'name'); // true
modelHasAttributes('User', 'nope'); // false
modelHasAttributes('User', ['id', 'name']); // true
modelHasAttributes('User', ['id', 'nope']); // false

Related

Laravel 8: How can I make an implicitly model bound route parameter optional instead of throwing 404?

I've tried to combine the Laravel docs on implicit binding and optional parameters and have the following code.
routes file:
Route::get('go/{example?}', [ExampleController::class, 'click'])->name('example');
And in the controller:
public function click(Example $example = null)
{
// Execution never reaches here
}
Execution never reaches the controller unless there is an Example with the correct slug, as it throws a 404. I want to check if $example is null in the controller and use custom logic there. How can this be accomplished?
Try this
Route::get('go/{example?}', [ExampleController::class, 'click'])->name('example');
public function click($example)
{
if($example != null){
$example = Example::findOrfail($example);
}
}
in model binding it will automatically run findOrfail to that model so don't you that so you will have control over it then you can manage
the #ettdro answer is perfect (and all credit is to him), but i think an answer with actual code would be useful:
routes:
Route::get('go/{example?}', [ExampleController::class, 'click'])->name('example');
controller:
public function click(Example $example)
{
// Stuff
}
Model of Example:
/**
* Retrieve the model for a bound value.
*
* #param mixed $value
* #param string|null $field
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
$result=$this->where('id', $value)->first();
return ($result)?$result:new Example();
}
You should obtain in the controller always a valid object, empty or not.
Had the same problem, and i'm happy with this solution.
To do that, you need use 'id' as your primary key in database and model,
if you are using another name for your pimary key, then you need to define it at your route:
Route::get('go/{example:number?}', [...]);

Laravel Eloquent - Override Model's constructor to change attributes

So I have a Page Model, which extends the Eloquent Model class. I am trying to override the constructor, where I need some additional logic. This is what I currently have:
class Page extends Model
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->main_image = null;
}
}
But this does not seem to save the main_image into the $this->attributes property when I call Page::find(1);.
I believe this is because Page::find eventually calls Model::newFromBuilder, which looks like this:
public function newFromBuilder($attributes = [], $connection = null)
{
$model = $this->newInstance([], true);
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
return $model;
}
So as you can see it creates the instance first and then sets the attributes, which means that anything set in the constructor gets ignored.
Is there any workaround for this to be able to override constructor (or similar method) to change the attributes for every retrieved/created model instance? Obviously I could override the newFromBuilder, newInstance, __construct and similar methods, but this seems very hacky and unmaintainable.
Thanks!
If all you need is to be able automatically modify a model's attribute when retrieved or set, then use Laravel Eloquent's Accesors and Mutators:
Defining An Accessor
To define an accessor, create a getFooAttribute method on your model where Foo is the "studly" cased name of the column you wish to access. In this example, we'll define an accessor for the first_name attribute. The accessor will automatically be called by Eloquent when attempting to retrieve the value of the first_name attribute:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get the user's first name.
*
* #param string $value
* #return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
As you can see, the original value of the column is passed to the accessor, allowing you to manipulate and return the value. To access the value of the accessor, you may simply access the first_name attribute on a model instance:
$user = App\User::find(1);
$firstName = $user->first_name;
Defining A Mutator
To define a mutator, define a setFooAttribute method on your model where Foo is the "studly" cased name of the column you wish to access. So, again, let's define a mutator for the first_name attribute. This mutator will be automatically called when we attempt to set the value of the first_name attribute on the model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Set the user's first name.
*
* #param string $value
* #return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
The mutator will receive the value that is being set on the attribute, allowing you to manipulate the value and set the manipulated value on the Eloquent model's internal $attributes property. So, for example, if we attempt to set the first_name attribute to Sally:
$user = App\User::find(1);
$user->first_name = 'Sally';
In this example, the setFirstNameAttribute function will be called with the value Sally. The mutator will then apply the strtolower function to the name and set its resulting value in the internal $attributes array.

In laravel how to pass extra data to mutators and accessors

What I'm trying to do is to append the comments of each article to the articles object, but the problem is that I need to request different number of comments each time.
and for some reason I need to use mutators for that, because some times I request 50 articles and I don't want to loop through the result and append the comments.
So is it possible to do something like the following and how to pass the extra argument.
This the Model:
class Article extends Model
{
protected $appends = ['user', 'comments', 'media'];
public function getCommentsAttribute($data, $maxNumberOfComments = 0)
{
// I need to set maxNumberOfComments
return $this->comments()->paginate($maxNumberOfComments);
}
}
Here is the controller:
class PostsController extends Controller
{
public function index()
{
//This will automatically append the comments to each article but I
//have no control over the number of comments
$posts = Post::user()->paginate(10);
return $posts;
}
}
What I don't want to do is:
class PostsController extends Controller
{
public function index()
{
$articles = Post::user()->all();
$number = 5;
User::find(1)->articles()->map(function(Article $article) {
$article['comments'] = $article->getCommnets($number);
return $article;
});
return Response::json($articles);
}
}
Is there a better way to do it? because I use this a lot and it does not seams right.
Judging from the Laravel source code, no – it's not possible to pass an extra argument to this magic accessor method.
The easiest solution is just to add another, extra method in your class that does accept any parameters you wish – and you can use that method instead of magic property.
Eg. simply rename your getCommentsAttribute() to getComments() and fire ->getComments() instead of ->comments in your view, and you are good to go.
I just set a public property on the model. At the accessing point, I update that property to my desired value. Then, in the attribute method, I read the desired arguments from that property. So, putting all of that together,
// Model.php
public $arg1= true;
public function getAmazingAttribute () {
if ($this->arg1 === false)
$this->relation()->where('col', 5);
else $this->relation()->where('col', 15);
}
// ModelController.php
$instance->arg1 = false;
$instance->append('amazing');
It is been a while for this question, but maybe someone will need it too.
Here is my way
{
/**
* #var string|null
*/
protected ?string $filter = null;
/**
* #return UserSettings[]|null
*/
public function getSettingsAttribute(): ?array
{
return services()->tenants()->settings($this)->getAll();
}
/**
* #return FeatureProperty[]|null
*/
public function getFeaturePropertiesAttribute(): ?array
{
return services()->tenants()->featureProperty($this)->getListByIds($this->filter);
}
/**
* #param string|null $filter
* #return Tenant
*/
public function filter(string $filter = null): Model
{
$this->filter = $filter;
return $this;
}
Accessor is using some service to get values. Service accepts parameters, in my case string, that will be compared with featureProperty->name
Magic happens when you return $this in filter method.
Regular way to call accessor would be:
$model->feature_properties
Extended way:
$model->filter('name')->feature_properties
Since filter argument can be null, we can have accessor like this:
$filter = null
$model->filter($filter)->feature_properties
In case you would like to play with it a little more you can think about overriding models getAttribute or magic __call methods implementing filter in manner which will be similar to laravel scopes
I know its an old question, but there is another option, but maybe not the best:
$articles = Post::user()->all();
$number = 5;
$articles->map(function($a) use($number){
$a->commentsLimit = $number;
return $a;
});
And then in getCommentsAttribute():
return $this->comments()->paginate($this->commentsLimit);

Laravel (eloquent) accessors: Calculate only once

I have a Laravel model which have a calculated accessor:
Model Job has some JobApplications which are associated to a User.
I want to get whether the user has already applied for a job or not.
For that I created an accessor user_applied which gets the applications relationships with the current user. This works okay, but the accessor is being calculated (making query) every time I access to the field.
Is there any easy way to calculate the accessor only once
/**
* Whether the user applied for this job or not.
*
* #return bool
*/
public function getUserAppliedAttribute()
{
if (!Auth::check()) {
return false;
}
return $this->applications()->where('user_id', Auth::user()->id)->exists();
}
Thanks in advance.
As suggested in a comment and really not tricky at all
protected $userApplied=false;
/**
* Whether the user applied for this job or not.
*
* #return bool
*/
public function getUserAppliedAttribute()
{
if (!Auth::check()) {
return false;
}
if($this->userApplied){
return $this->userApplied;
}else{
$this->userApplied = $this->applications()->where('user_id', Auth::user()->id)->exists();
return $this->userApplied;
}
}
I’d instead create a method on your User model that you pass a Job to, and returns a boolean as to whether the user’s applied or not:
class User extends Authenticatable
{
public function jobApplications()
{
return $this->belongsToMany(JobApplication::class);
}
public function hasAppliedFor(Job $job)
{
return $this->jobApplications->contains('job_id', $job->getKey());
}
}
Usage:
$applied = User::hasAppliedFor($job);
You can set user_applied value to the model->attributes array and return it from attributes array on the next access.
public function getUserAppliedAttribute()
{
$user_applied = array_get($this->attributes, 'user_applied') ?: !Auth::check() && $this->applications()->where('user_id', Auth::user()->id)->exists();
array_set($this->attributes, 'user_applied', $user_applied);
return $user_applied;
}
The array_get will return null when accessed first time, which will cause the next side of ?: to be executed. The array_set will set the evaluated value to the 'user_applied' key. In the subsequent calls, array_get will return previously set value.
The bonus advantage with this approach is that if you've set user_applied somewhere in your code (eg Auth::user()->user_applied = true), it will reflect that, meaning it will return that value without doing any additional thing.

How to change empty string to a null using Laravel 5.1?

While using Laravel 5.1, I am trying to check every value before it is saved in the database using Eloquent ORM. My logic is, first trim the value, if the value is an empty string "", then to convert it to null instead of just an empty string.
I was advised to create a Trait which will override the setAttribute method for that.
So here is what I have done
I have a new folder "app\Traits" inside of a file called TrimScalarValues.php which contains the following code
<?php
namespace App\Traits;
trait TrimScalarValues
{
public function setAttribute($key, $value)
{
if (is_scalar($value)) {
$value = $this->emptyStringToNull(trim($value));
}
return $this->setAttribute($key, $value);
}
/**
* return null value if the string is empty otherwise it returns what every the value is
*
*/
private function emptyStringToNull($string)
{
//trim every value
$string = trim($string);
if ($string === ''){
return null;
}
return $string;
}
}
Finally I have a app\Models\Account.php file which contains the following code
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\industry;
use App\Traits\RecordSignature;
use App\Traits\TrimScalarValues;
class Account extends Model
{
use RecordSignature, TrimScalarValues;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'accounts';
protected $primaryKey = 'account_id';
const CREATED_AT = 'created_on';
const UPDATED_AT = 'modified_on';
const REMOVED_AT = 'purged_on';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['client_id','account_name', 'company_code', 'legal_name', 'created_by','modified_by','instrucations'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
//protected $hidden = ['account_id', 'remember_token'];
protected $guarded = ['account_id'];
/**
* Get the industry record associated with the account.
*/
public function industry()
{
return $this->hasOne(industry, industry::primaryKey);
}
public function pk(){
return $this->primaryKey;
}
}
But every time I update a value, I get a white page with no error or logs.
When I modify the app\Models\Account.php and change use RecordSignature, TrimScalarValues; to use RecordSignature; then I do not get a white page but obviously the values are not trimmed and converted to null.
What am I doing wrong here?
You can't call $this->setAttribute() in your trait. Instead you want to call the "original" setAttribute method by using parent:::
public function setAttribute($key, $value)
{
if (is_scalar($value)) {
$value = $this->emptyStringToNull(trim($value));
}
return parent::setAttribute($key, $value);
}
Regarding the empty logs, have you checked the webserver log besides the one from the framework?
I had the same problem and solved it by creating a middleware that filters empty input fields.
public function handle($request, Closure $next) {
$input = $request->all();
if ($input) {
array_walk_recursive($input, function (&$item) {
$item = trim($item);
$item = ($item == "") ? null : $item;
});
$request->merge($input);
}
return $next($request);
}
Don't forget to add your custom middleware to Http/Kernel.php
Found this at Laracasts
You might wanna take a look at this package:
https://packagist.org/packages/iatstuti/laravel-nullable-fields
"This create a trait and allows you to easily flag attributes that should be set as null when being persisted to the database."
I know this post is old, and at that time this package maybe didn't exist yet.
You can use mutator in you model.
For the field account_name mutator should looks like this:
public function setAccountNameAttribute($account_name)
{
if(is_null($account_name))
{
$this->attributes['account_name'] = null;
}
else
{
$this->attributes['account_name'] = $account_name;
}
}
And everytime when you will update or insert the record using Eloquent, account_name will be passed through this mutator.

Categories