How to set multiple boolean mutators - laravel

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');

Related

How to make it easier to access options in 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

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();
}
}

How to write and use a custom Laravel 5 Validator to validate multiple other fields values

I have fields in a form that are only required IF two other fields are set TO SPECIFIC VALUES. This is not a case of required_with_all. ITs not if they are set, its if they are set specifically.
Example: 'foo' => 'required_if_all:bar,2,bat,1',
I added a service provider:
<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Validator;
class RequiredIfAllProvider extends ServiceProvider {
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
Validator::extend('required_if_all', function($attribute,$value,$parameters){
// Is required if the list of key/value pairs are matching
$pairs = [];
foreach($parameters as $kp => $vp) $pairs[$kp] = $vp;
foreach($pairs as $kp => $vp) if(\Request::input($kp) != $vp) return false;
return true;
});
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
//
}
}
And I make sure to use the App\Providers\RequiredIfAllProvider; in the top of my custom Request file.
If bar and bat are both set based on the parameters based to the validation, a new error should be added to the error bag.
I have spent quite a bit on this. Any ideas?
Register the service provider in config\app.php under providers, no need to use it in the request class. (Docs/Registering Providers)
Don't get the other attributes from the Input facade! This limits the use of your validator heavily and could lead to weird bugs. The fourth parameter passed to the validation callback is the validator instance. It has a getData() method giving you all the data your validator is currently validating.
As your rule should also be run on empty values, you need to register it with the extendImplicit() method. (Docs\Custom Validation Rules)
Untested example code:
public function boot()
{
Validator::extendImplicit('required_if_all', function($attribute, $value, $parameters, $validator) {
// No further checks required if value is there
if ($value) {
return true;
}
// Convert $parameters into a named array with the attributes as keys
$n_pairs = floor(count($parameters)/2);
$pairs = [];
for ($i = 0; $i < $n_pairs; $i++) {
$pairs[$parameters[$i]] = $parameters[$i+1];
}
// Check if all pairs match with the input
$data = $validator->getData();
foreach ($pairs as $key => $value) {
if ($data[$key] !== $value) {
// If at least one pair does not match, the rule is always true
return true;
}
}
// All pairs match, now $value has to be set
return !!$value;
});
}
I believe the best practice would be using a Form Request as explained in the docs.
And you can use required_with_all and such in validation as explained here

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;
}

Resources