How to make it easier to access options in Laravel - laravel

There is a database structure like this:
users
id
name
user_options
id
user_id
option_name
option_value
There can be a lot of options, so I didn’t make a separate field for each of them.
I would like to make it easier to access an option by its name.
Currently implemented like this:
In the User model:
public function options() : HasMany
{
return $this->hasMany(UserOptions::class);
}
For the test, I write the code directly in web.php in routes:
$user = User::with('options')->first();
$theme = $user
->options
->firstWhere('option_name', '=', 'theme')
->option_value;
There are many options and I would not like to make such a voluminous appeal to each of them.
Please help me to simplify access to options

If you absoltely must access like an object you could add an attribute accessor on your user model like so:
protected function options(): Attribute
{
return Attribute::make(
get: function($_){
if($this->userOptions !== null){ // have the user options not already been set?
$this->setUserOptions() // set them if not
}
return $this->userOptions // return them
}
)
}
private function setUserOptions(): void
{
$this->userOptions = new stdClass()
foreach(UserOptions::where('user_id', $this->id)->get() as $option){
$optionName = $option['option_name']
$optionValue = $option['option_value']
$this->userOptions->$optionName = $optionValue
}
}
Call like
$user->options->theme
But be way of nonexistant options
A much less complex way would be adding a helper function on your user Model though like so:
/**
* #return Collection<UserOption>
*/
public function options(): HasMany
{
return $this->hasMany(UserOption::class, 'user_id', 'id');
}
/**
* #param string $optionName
* #return mixed
*/
public function getOption(string $optionName): mixed
{
/** #var UserOption $option */
foreach($this->options as $option){
if($option->option_name === $optionName){
return $option['option_value'];
}
}
return null;
}
And simply call like $user->getOption('color'); // eg: "red" | null

Related

Laravel Eloquent - setup both-ways relationships after loading?

Let's consider the following example: a thread has posts, and the posts also have a "thread" relation. The title of each post must include the title of the parent thread.
class Thread extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
class Post extends Model
{
public function thread()
{
return $this->belongsTo(Thread::class);
}
public function getTitleAttribute(string $title): string
{
return $this->thread->title . ': ' . $title;
}
}
What I want to achieve:
//when we load the posts using the thread...
$posts = $thread->posts;
//...I want the "thread" relation of each post to be automatically set to $thread, so that:
$posts->first()->thread === $thread //true
By default it's not true. And if we do this:
$array = $thread->posts->toArray();
this will cause loading of the thread for each post one by one from DB which is super non-optimal. Is there some elegant Laravel technique to setup relations of the just loaded models?
You can lazy load them like this
$posts = $thread->posts()->with('thread')->get();
If you dont want the extra query, you can use map()
$thread->posts->map(function($post) use ($thread) {
return $post->setRelation('thread', $thread);
});
This will lead to the same amount of object but will also lead to loop of references.
//this is defined and doesn't use more object or launch other queries
$thread->posts->first()->thread->posts()->first()->thread;
if you want to Automate it, I suggest you create a function on Thread model to get the posts threaded.
public function loadThreadedPosts()
{
$this->posts->map(function($post) {
return $post->setRelation('thread', $this);
});
return $this;
}
//then you can
$thread->loadThreadedPosts()->posts;
If you want it to automatically be done when you specifically call for the relation "posts" on the Thread::class model, add this method to your Thread::class to overwrite the function present in the Trait HasAttributes at your own risk
/**
* Get a relationship value from a method.
*
* #param string $method
* #return mixed
*
* #throws \LogicException
*/
protected function getRelationshipFromMethod($method)
{
$relation = $this->$method();
if (! $relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
));
}
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.', static::class, $method
));
}
return tap($relation->getResults(), function ($results) use ($method) {
if ($method == "posts") {
$results->map(function($post) {
return $post->setRelation('thread', $this);
});
}
$this->setRelation($method, $results);
});
}
Hope you understand that this overwrites a vendor method and might lead to future issues, also I dont think that this one method works with eager loading (for example: Thread::with('posts')->get()) and I dont know what else might get broken/have unexpected behavior.
As I said, at your own risk (bet/hope ->loadThreadedPosts() looks more interesting now)

Laravel/Livewire: Use withTrashed() on model route binding on to show deleted records

In the list I display the latest topic, including those that is deleted.
function latest()
{
return Topic::withTrashed()->latest();
}
For displaying a single topic I have a Livewire component with that topic passed into it.
class ShowTopic extends Component
{
public $topic;
public function mount(Topic $topic)
{
$this->topic = $topic;
}
public function render()
{
return view('livewire.show-topic', [
'topic' => $this->topic,
]);
}
}
But when I go to a single topic that is deleted, it doesn't show. How can I use withTrashed() on model route bindings to show deleted records with my Livewire component?
You can overwrite the resolveRouteBinding() method on your Eloquent model, and conditionally remove the SoftDeletingScope global scope.
Here I'm using a policy for that model to check if I can delete the model - and if the user can delete it, they can also see it. You could implement any logic you want, or remove the global scope for all requests if that is more suitable for your application.
use Illuminate\Database\Eloquent\SoftDeletingScope;
class Topic extends Model {
// ...
/**
* 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)
{
// If no field was given, use the primary key
if ($field === null) {
$field = $this->getKey();
}
// Apply where clause
$query = $this->where($field, $value);
// Conditionally remove the softdelete scope to allow seeing soft-deleted records
if (Auth::check() && Auth::user()->can('delete', $this)) {
$query->withoutGlobalScope(SoftDeletingScope::class);
}
// Find the first record, or abort
return $query->firstOrFail();
}
}

What is the right way to create a global function in Laravel 5

I use Auth in all my models to convert datetimes to the user's timezone when retrieved. Now I ran into a situation where the user is not authenticated, and this conversion needs to be done in my model.
My goal would be to retrieve a default timezone from another table like so:
$business = Business::where('id', '=', $id)->first(); // default timezone is in here: $business->timezone
Since I use \Auth::user()->timezone in all my model, is there a place where I should add the code above (I'll need to pass $id which is not available when user is authenticated) for the retrieval of the default timezone, so I can access $business->timezone globally, only when a user is not authenticated?
This is the code I currently have in one of my models.
/**
* Set event_start attribute.
*/
public function setEventStartAttribute($event_start)
{
$this->attributes['event_start'] = Carbon::createFromFormat('Y-m-d H:i', $event_start, \Auth::user()->timezone)->setTimezone('UTC');
}
/**
* Set event_end attribute.
*/
public function setEventEndAttribute($event_end)
{
$this->attributes['event_end'] = Carbon::createFromFormat('Y-m-d H:i', $event_end, \Auth::user()->timezone)->setTimezone('UTC');
}
/**
* Get event_end attribute.
*/
public function getEventEndAttribute($value)
{
$format = $this->getDateFormat();
return Carbon::createFromFormat($format, $value, 'UTC')->setTimezone( \Auth::user()->timezone);
}
/**
* Get event_end attribute.
*/
public function getEventEndAttribute($value)
{
$format = $this->getDateFormat();
return Carbon::createFromFormat($format, $value, 'UTC')->setTimezone( \Auth::user()->timezone);
}
I guess my code would then be changed to something like:
/**
* Set event_start attribute.
*/
public function setEventStartAttribute($event_start)
{
if (Auth::check()) {
$this->attributes['event_start'] = Carbon::createFromFormat('Y-m-d H:i', $event_start, \Auth::user()->timezone)->setTimezone('UTC');
}
else {
$this->attributes['event_start'] = Carbon::createFromFormat('Y-m-d H:i', $event_start, $business->timezone)->setTimezone('UTC');
}
}
Thanks
In this case, Cache is an option for default timezone. Then you create a class that will serve this information statically.
Create a class like:
public class MyTimezone {
public static function getTimezone(){
if(\Auth::check()) {
$timezone = \Auth::user()->timezone;
} else {
$timezone = Cache::get('default_timezone');
if(empty($timezone)) {
//query business here
$timezone = $business->timezone;
Cache::put('default_timezone', $business->timezone); //cache here and "never" query again
}
}
return $timezone;
}
}
And use like this:
$this->attributes['event_start'] = Carbon::createFromFormat('Y-m-d H:i', $event_start, MyTimezone::getTimezone())->setTimezone('UTC');
Create a custom helper and make it accessible from anywhere in your application, take a look at this post:
Best practices for custom helpers on Laravel 5

Laravel Model conditional formatting

I have a database and model called Vote_actions that looks like this:
id
group_id
user_id
action_type
anonymous (boolean)
User can ask to be anonymous (that would make the boolean value to be true).If that is the case, I want to change the group_id and user_id from the returned model to -1.
Is there a way in laravel that I can do it ?
I know this question is old. I was looking for a way to hide some fields on certain conditions, external conditions like Auth Roles, and internal conditions like Model attributes, and I found a very flexible way to hide them.
And since I saw the other OP's duplicated post Laravel Hidden Fields On Condition asking for hiding field instead, So I'm gonna share it with you.
I know a mutator can change the value of its field, but to Hide it, you need :
the $hidden array attribute
the constructor __Construct() (optional)
to override method newFromBuilder method of Laravel Model
Here are the processes in the Model app\Vote_actions.php:
Hidden. Let's say you normally want to hide the fields created_at and updated_at of Laravel, you use:
protected $hidden = ['created_at', 'updated_at'];
External Conditions. Now let's say if the Authenticated User is Staff you want to unhide them:
public function __Construct()
{
parent::__construct();
if(\Auth::check() && \Auth::user()->isStaff()) {
// remove all fields so Staff can access everything for example
$this->hidden = [];
} else {
// let's hide action_type for Guest for example
$this->hidden = array_merge($this->hidden, ['action_type'];
}
}
Internal Conditions Let's say now you want to hide anonymous field is its value is true:
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #param array $connection
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = array(), $connection = null)
{
$instance = parent::newFromBuilder($attributes, $connection);
if((bool)$instance->anonymous === true) {
// hide it if array was already empty
// $instance->hidden = ['anonymous'];
// OR BETTER hide single field with makeHidden method
$instance->makeHidden('anonymous');
// the opposite is makeVisible method
}
return $instance;
}
You can't play with hidden attributes and method inside mutators, that's their weakness when we need to hide instead of changing values.
But in any case, understand that calling modification on high load of hundredths of rows can be costly in time.
You are leaning towards an edge case, with special conditions.
Make use of accessors:
class VoteActions extends \Eloquent {
public $casts = [
'anonymous' => 'boolean'
];
...
/**
* Accessors: Group ID
* #return int
*/
public function getGroupIdAttribute()
{
if((bool)$this->anonymous === true) {
return -1;
} else {
return $this->group_id;
}
}
/**
* Accessors: User ID
* #return int
*/
public function getUserIdAttribute()
{
if((bool)$this->anonymous === true) {
return -1;
} else {
return $this->user_id;
}
}
}
Official Documentation: https://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
However, i would recommend that you set the value in the database directly to -1 where necessary so as to preserve the integrity of your application.
Of course you can easily do that. Read about accessors (getters):
https://laravel.com/docs/5.1/eloquent-mutators
Example:
function getUserIdAttribute()
{
return $this->anonymous ? -1 : $this->user_id;
}
function getGroupIdAttribute()
{
return $this->anonymous ? -1 : $this->group_id;
}

How to set multiple boolean mutators

I'm using Laravel 4 and I have a model with a lot of boolean attributes.
For each of them I'm setting a setter like this
public function setIsRemoteAttribute($value){
$this->attributes['isRemote'] = !!$value;
}
and a getter like this
public function getIsRemoteAttribute($value){
return !! $this->attributes['isRemote'];
}
Is there any way to abstract that out so I'm not individually setting 12+ mutators?
I guess you can override setAttribute method like:
public function setAttribute($key, $value){
if(in_array($key, 'abstract_keys')){
$this->attributes[$key] = !!$value;
}
else{
parent::setAttribute($key, $value);
}
}
Same would go for getAttribute.
I have L5 installation but I'm pretty sure this will apply to L4.2 as well.
If you look in the code for Eloquent's Model class you will find the following method:
/**
* Set a given attribute on the model.
*
* #param string $key
* #param mixed $value
* #return void
*/
public function setAttribute($key, $value)
{
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key))
{
$method = 'set'.studly_case($key).'Attribute';
return $this->{$method}($value);
}
// If an attribute is listed as a "date", we'll convert it from a DateTime
// instance into a form proper for storage on the database tables using
// the connection grammar's date format. We will auto set the values.
elseif (in_array($key, $this->getDates()) && $value)
{
$value = $this->fromDateTime($value);
}
if ($this->isJsonCastable($key))
{
$value = json_encode($value);
}
$this->attributes[$key] = $value;
}
You could potentially, override this function in your own model:
Store a list of attributes that should get the boolean mutator
Check if $key is within this list of elements
If it is - do something
If it's not, default to the parent implementation (This method)
Example:
public function setAttribute($key, $value)
{
if (in_array($key, $this->booleans))
{
// Do your stuff here - make sure to return it
}
return parent::setAttribute($key, $value);
}
You can do the same thing for the getAttribute method.
With this approach, all you need to do is add the names of the attributes to the list of booleans for them to work.
protected $booleans = array('attr1', 'attr2');

Resources