Reusing an accessors & mutators - laravel

In several of my models I have code like this
public function setTotalAttribute($value)
{
return $this->attributes['total'] = $value * 100;
}
public function getTotalAttribute($value)
{
return $value * 0.01;
}
Sometimes the field that I am mutating is called purchase or price, but the code is the same (changing 7.99 to 799 to store in the DB, and change it back on return).
If all the fields were named the same I could use a trait, however they are slightly different.... is there a way I can setup something similar to the date fields which auto-mutate to Carbon instances?

One solution is to define the fields that deal with dollars/cents conversion in the models that have such fields, and then use a trait to override the global mutators/accessors.
class Model
{
use HasMoneyFields;
protected $moneyFields = ['purchase', 'price', 'total'];
}
trait HasMoneyFields
{
public function getAttributeValue($key)
{
$value = parent::getAttributeValue($key);
if (property_exists($this, 'moneyFields')) {
if (in_array($key, $this->moneyFields)) {
$value /= 100;
}
}
return $value;
}
public function setAttribute($key, $value)
{
parent::setAttribute($key, $value);
if (property_exists($this, 'moneyFields')) {
if (in_array($key, $this->moneyFields)) {
$this->attributes[$key] = $value * 100;
}
}
}
}

You might be interested in https://github.com/topclaudy/eloquent-mutators
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use \Awobaz\Mutator\Mutable;
protected $accessors = [
'title' => 'trim_whitespace',
'content' => 'trim_whitespace',
];
}
The package allows you to create custom accessors/mutators extensions.

Related

Add 5 minutes to timestamp after each model insert using Factory

When inserting models into the database I'd like to add 5 minutes to the timestamp property after each single model insert.
I thought that using Sequence like this would do the trick but it's not working: all models that are inserted still have the same timestamp.
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Factories\Sequence;
class TestSeeder extends Seeder
{
public function run()
{
\App\Models\Test::factory(1000)
->state(new Sequence(
fn () => ['timestamp' => now()->addMinutes(5)->toDateTimeString()],
))
->create();
}
}
I will suggest this approach maybe with the need for some adjustments:
class TestSeeder extends Seeder
{
private $testData = [];
public function run()
{
$now = now();
for ($i=0; $i < 100; $i++) {
$testData[] = [
'key1' => Str::random(10),
'key2' => Str::random(10),
'timestamp' => $now->addMinutes(5)->toDateTimeString()
];
}
foreach ($testData as $test) {
\App\Models\Test::create($test);
}
}
}
Use Factory Callbacks;
class TestFactory extends Factory
{
protected $model = Test::class;
/**
* Configure the model factory.
*
* #return $this
*/
public function configure()
{
return $this->afterCreating(function (Test $test) {
$test->update(['created_at' => Test::last()->created_at->addMinutes(5)]);
});
}
}
Seeder Class
public function run()
{
\App\Models\Test::factory(1000)->create();
}
You can use Eloquent Model Events: https://laravel.com/docs/8.x/eloquent#events
Ex:
class User extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::created(function ($user) {
// update the time or any fields else
});
}
}

Laravel - Delete if no relationship exists

Below is the one of the model. I would like to delete a Telco entry only if no other model is referencing it? What is the best method?
namespace App;
use Illuminate\Database\Eloquent\Model;
class Telco extends Model
{
public function operators()
{
return $this->hasMany('App\Operator');
}
public function packages()
{
return $this->hasMany('App\Package');
}
public function topups()
{
return $this->hasMany('App\Topup');
}
public function users()
{
return $this->morphMany('App\User', 'owner');
}
public function subscribers()
{
return $this->hasManyThrough('App\Subscriber', 'App\Operator');
}
}
You can use deleting model event and check if there any related records before deletion and prevent deletion if any exists.
In your Telco model
protected static function boot()
{
parent::boot();
static::deleting(function($telco) {
$relationMethods = ['operators', 'packages', 'topups', 'users'];
foreach ($relationMethods as $relationMethod) {
if ($telco->$relationMethod()->count() > 0) {
return false;
}
}
});
}
$relationships = array('operators', 'packages', 'topups', 'users', 'subscribers');
$telco = Telco::find($id);
$should_delete = true;
foreach($relationships as $r) {
if ($telco->$r->isNotEmpty()) {
$should_delete = false;
break;
}
}
if ($should_delete == true) {
$telco->delete();
}
Well, I know this is ugly, but I think it should work. If you prefer to un-ugly this, just call every relationship attributes and check whether it returns an empty collection (meaning there is no relationship)
If all relationships are empty, then delete!
After seeing the answers here, I don't feel copy pasting the static function boot to every models that need it. So I make a trait called SecureDelete. I put #chanafdo's foreach, inside a public function in SecureDelete.
This way, I can reuse it to models that need it.
SecureDelete.php
trait SecureDelete
{
/**
* Delete only when there is no reference to other models.
*
* #param array $relations
* #return response
*/
public function secureDelete(String ...$relations)
{
$hasRelation = false;
foreach ($relations as $relation) {
if ($this->$relation()->withTrashed()->count()) {
$hasRelation = true;
break;
}
}
if ($hasRelation) {
$this->delete();
} else {
$this->forceDelete();
}
}
}
Add use SecureDelete to the model that needs it.
use Illuminate\Database\Eloquent\Model;
use App\Traits\SecureDelete;
class Telco extends Model
{
use SecureDelete;
public function operators()
{
return $this->hasMany('App\Operator');
}
// other eloquent relationships (packages, topups, etc)
}
TelcoController.php
public function destroy(Telco $telco)
{
return $telco->secureDelete('operators', 'packages', 'topups');
}
In addition, instead of Trait, you can also make a custom model e.g BaseModel.php that extends Illuminate\Database\Eloquent\Model, put the function secureDelete there, and change your models to extends BaseModel.

Laravel - how to makeVisible an attribute in a Laravel relation?

I use in my model code to get a relation
class User extends Authenticatable
{
// ...
public function extensions()
{
return $this->belongsToMany(Extension::class, 'v_extension_users', 'user_uuid', 'extension_uuid');
}
// ...
}
The Extension has field password hidden.
class Extension extends Model
{
// ...
protected $hidden = [
'password',
];
// ...
}
Under some circumstances I want to makeVisible the password field.
How can I achieve this?
->makeVisible([...]) should work:
$model = \Model::first();
$model->makeVisible(['password']);
$models = \Model::get();
$models = $models->each(function ($i, $k) {
$i->makeVisible(['password']);
});
// belongs to many / has many
$related = $parent->relation->each(function ($i, $k) {
$i->makeVisible(['password']);
});
// belongs to many / has many - with loading
$related = $parent->relation()->get()->each(function ($i, $k) {
$i->makeVisible(['password']);
});
Well, I got the idea from https://stackoverflow.com/a/38297876/518704
Since my relation model Extension::class is called by name in my code return $this->belongsToMany(Extension::class,... I cannot even pass parameter to it's constructor.
So to pass something to the constructor I may use static class variables.
So in my Extension model I add static variables and run makeVisible method.
Later I destruct the variables to be sure next calls and instances use default model settings.
I moved this to a trait, but here I show at my model example.
class Extension extends Model
{
public static $staticMakeVisible;
public function __construct($attributes = array())
{
parent::__construct($attributes);
if (isset(self::$staticMakeVisible)){
$this->makeVisible(self::$staticMakeVisible);
}
}
.....
public function __destruct()
{
self::$staticMakeVisible = null;
}
}
And in my relation I use something like this
class User extends Authenticatable
{
...
public function extensions()
{
$class = Extension::class;
$class::$staticMakeVisible = ['password'];
return $this->belongsToMany(Extension::class, 'v_extension_users', 'user_uuid', 'extension_uuid');
}
...
}
The highest voted answer didn't seem to work for me (the relations attribute seems to be a protected array now so can't be used as a collection in #DevK's answer), I instead used:
$parent->setRelation('child', $parent->child->first()->setVisible(['id']));

Save all values in Uppercase Laravel 5+

What would be the best way to save all values in the database as uppercase. So before saving convert all strings to uppercase. I see options to use Events or a trait would probably be best but not quite sure how to achieve this. I do not want to create accessors & mutators for each of my fields.
Got this from : https://laracasts.com/discuss/channels/eloquent/listen-to-any-saveupdatecreate-event-for-any-model
trait Trackable {
public static function bootTrackable()
{
static::creating(function ($model) {
// blah blah
});
static::updating(function ($model) {
// bleh bleh
});
static::deleting(function ($model) {
// bluh bluh
});
}
}
Im not sure how I would be able to get the actual request values to convert them to uppercase?
As #Louwki said, you can use a Trait to do that, in my case I did something like this:
trait SaveToUpper
{
/**
* Default params that will be saved on lowercase
* #var array No Uppercase keys
*/
protected $no_uppercase = [
'password',
'username',
'email',
'remember_token',
'slug',
];
public function setAttribute($key, $value)
{
parent::setAttribute($key, $value);
if (is_string($value)) {
if($this->no_upper !== null){
if (!in_array($key, $this->no_uppercase)) {
if(!in_array($key, $this->no_upper)){
$this->attributes[$key] = trim(strtoupper($value));
}
}
}else{
if (!in_array($key, $this->no_uppercase)) {
$this->attributes[$key] = trim(strtoupper($value));
}
}
}
}
}
And in your model, you can specify other keys using the 'no_upper' variable. Like this:
// YouModel.php
protected $no_upper = ['your','keys','here'];
Was a lot easier than I through. Solution that is working for me using traits, posting it if anyone also run into something like this.
<?php
namespace App\Traits;
trait SaveToUpper
{
public function setAttribute($key, $value)
{
parent::setAttribute($key, $value);
if (is_string($value))
$this->attributes[$key] = trim(strtoupper($value));
}
}
}
UPDATE:
For Getting values as upper case you can add this to the trait or just add it as a function in the model:
public function __get($key)
{
if (is_string($this->getAttribute($key))) {
return strtoupper( $this->getAttribute($key) );
} else {
return $this->getAttribute($key);
}
}

Laravel: override eloquent function for get results?

I can override function before save :
public function save(array $options = [])
{
if(isset($this->datesConvert)){
foreach($this->datesConvert as $date){
$this->attributes[$date] = Carbon::createFromFormat('d/m/Y', $this->attributes[$date])->format('Y-m-d');
}
}
parent::save($options);
}
But which method I can use for get result ? and where is documentation for this. I am looking for something like :
public function get()
{
parent::get();
if(isset($this->datesConvert)){
foreach($this->datesConvert as $date){
$this->attributes[$date] = Carbon::createFromFormat('Y-m-d', $this->attributes[$date])->format('d/m/Y');
}
}
}
With that I can convert 10 date rows without need of 20 mutators..
It seems that Attribute casting fits your needs or use Date mutators
You may customize which fields are automatically mutated, and even completely disable this mutation, by overriding the $dates property of your model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be mutated to dates.
*
* #var array
*/
protected $dates = [
'created_at',
'updated_at',
'deleted_at',
// more dates
];
}
EDIT
Another way, you can override getAttribute method in Model
<?php
namespace App;
use Carbon\Carbon;
trait DateFormatting
{
protected function dateFields()
{
return [];
}
public function getAttribute($key)
{
if ( array_key_exists( $key, $this->dateFields() ) ) {
return Carbon::createFromFormat('d/m/Y', $this->attributes[$key])->format('Y-m-d');
}
return parent::getAttribute($key);
}
}
then you can use this trait in any your model, just don't forget override dateFields in it
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\DateFormatting;
class User extends Model
{
use DateFormatting;
protected function dateFields()
{
return [
'finished_at',
// other field names that you need to format
];
}
after all you can access to this fields as usual(using magic __get())
$model->finished_at;
I find a solution, My solution is :
public function save(array $options = [])
{
if(isset($this->datesConvert)){
foreach($this->datesConvert as $date){
$this->attributes[$date] = \Carbon\Carbon::createFromFormat('d/m/Y', $this->attributes[$date])->format('Y-m-d');
}
}
parent::save($options);
}
public function getAttribute($key)
{
$value = parent::getAttribute($key);
if(isset($this->attributes[$key])){
if(isset($this->datesConvert) && in_array($key, $this->datesConvert)){
$value = \Carbon\Carbon::createFromFormat('Y-m-d', $value)->format('d/m/Y');
}
}
return $value;
}
Laravel comes with something very useful for this problem. I'm not sure what it's called, but you can modify attributes or even add new attributes like this:
class YourModel extends Model
{
...
public function getDateAttribute()
{
return Carbon::createFromFormat('Y-m-d', $this->attributes[$date])->format('d/m/Y');
}
...
}
You can retrieve this attribute like:
$yourModel->date;
Edit after comment #fico7489
You can't ignore the fact you always have to modify things. However there are still some solutions to make it easier.
For example you can change your date column to a string and just store your date with the desired date format.
Other solution should be modifying the date through sql. FORMAT(Now(),'YYYY-MM-DD').
Example in laravel would look like (not tested):
YourModel::select([
'*',
DB::raw('
FORMAT(yourDateColumn,'YYYY-MM-DD')
')
])->get();

Resources