How to use not static attributes as default values in Eloquent? - laravel

There's classes and schedules. Strictly, one SchoolClass to one Schedule.
The creation of a Schedule depends on a SchoolClass, so I decided to inject SchoolClass on constructor to constrain the association.
class Schedule extends Model
{
// ...
private $class;
public function __construct(SchoolClass $class)
{
$this->class = $class;
}
// Used to limit domain, based on the associated class_id
protected static function booted()
{
static::addGlobalScope('class', function (Builder $builder) {
$builder->where('class_id', $this->class->id);
});
}
public function getClassIdAttribute()
{
return $this->class->id;
}
// ...
}
By this way, I can obtain the class_id associated with the Schedule
new Schedule(SchoolClass::find(1))->class_id; // Returns 1, as supposed
When creating a Schedule, associated to a SchoolClass, I want that builder consider the class_id attribute, but when I try to save it:
new Schedule(SchoolClass::find(1))->save(); // General error: 1364 Field 'class_id' doesn't have a default value

You've probably not set class_id in the fillable array. If you do that or make an empty guarded array, the error should be resolved.

I found a possible solution.
Updating model $attributes on the constructor:
public function __construct(SchoolClass $class)
{
$this->class = $class;
$this->attributes['class_id'] = $class->id;
}
So, until now the model wasn't fullfiled with class_id, and wasn't considering that field on save() method.
By the way, I believe there will be a better solution.

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)

Assign value to custom property in model

How to assign value to my custom property ? I am comming from Yii2 background and there is pretty straight forward. Here I made this:
protected $appends = ['sites'];
public function getSitesAttribute()
{
return $this->sites;
}
public function setSitesAttribute($value)
{
$this->attributes['sites'] = $value;
}
Trying to assign like mymodel->sites = ['1', '2', '3'] but without success. After trying to get the mymodel->sites value error is thrown: undefined property 'sites'.
Your accessor is not quite correct, note that you're not returning $this->attributes['sites']; but instead $this->sites.
class MyModel extends Model
{
public function getSitesAttribute()
{
return $this->attributes['sites'];
}
public function setSitesAttribute($value)
{
$this->attributes['sites'] = $value;
}
}
Then;
$my_model->sites = [1,2,3];
$my_model->sites; // [1,2,3]
You might want to set an initial default value for $this->attributes['sites'] in the model constructor.
The problem lies with the $this->sites. It is correct that it will throw an exception that you class instance does not have a property $sites.
Take a look at the __get method of Model. You can see here that a getAttribute($key) call is being made. Diving even deeper you would find out that at some point the following code is being executed in the transformModelValue method:
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
This piece of code calls your getSitesAttribute method.
Going back a little bit and you will see that it tries to retrieve the value for the $key (sites) from the attributes array by calling the getAttributeFromArray($key). This will return the value stored in the attributes array which is where your setSitesAttribute mutator stores the value.
The following options I could come up with:
Remove the accessor to let the model retrieve the value from the $attributes array.
protected $appends = ['sites'];
public function setSitesAttribute($value)
{
$this->attributes['sites'] = $value;
}
Store value in and retrieve from $sites property. Not sure if $appends still works.
protected $appends = ['sites'];
private $sites = [];
public function getSitesAttribute()
{
return $this->sites;
}
public function setSitesAttribute($value)
{
$this->sites = $value;
}

lararvel uuid as primary key

I'm trying to set an uuid as primary key in a Laravel Model. I've done it setting a boot method in my model as stablished here so I don't have to manually create it everytime I want to create and save the model. I have a controller that just creates the model and saves it in database.
It is saved correctly in database but when controller returns the value of the id is always returned with 0. How can I make it to actually return the value that it is creating in database?
Model
class UserPersona extends Model
{
protected $guarded = [];
protected $casts = [
'id' => 'string'
];
/**
* Setup model event hooks
*/
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$uuid = Uuid::uuid4();
$model->id = $uuid->toString();
});
}
}
Controller
class UserPersonaController extends Controller
{
public function new(Request $request)
{
return UserPersona::create();
}
}
You need to change the keyType to string and incrementing to false. Since it's not incrementing.
public $incrementing = false;
protected $keyType = 'string';
Additionally I have an trait which I simply add to those models which have UUID keys. Which is pretty flexible. This comes originally from https://garrettstjohn.com/articles/using-uuid-laravel-eloquent-orm/ and I added some small adjustments to it for issues which I have discovered while using it intensively.
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
/**
* Class Uuid.
* Manages the usage of creating UUID values for primary keys. Drop into your models as
* per normal to use this functionality. Works right out of the box.
* Taken from: http://garrettstjohn.com/entry/using-uuids-laravel-eloquent-orm/
*/
trait UuidForKey
{
/**
* The "booting" method of the model.
*/
public static function bootUuidForKey()
{
static::retrieved(function (Model $model) {
$model->incrementing = false; // this is used after instance is loaded from DB
});
static::creating(function (Model $model) {
$model->incrementing = false; // this is used for new instances
if (empty($model->{$model->getKeyName()})) { // if it's not empty, then we want to use a specific id
$model->{$model->getKeyName()} = (string)Uuid::uuid4();
}
});
}
public function initializeUuidForKey()
{
$this->keyType = 'string';
}
}
Hope this helps.
Accepted answer not worked for me on Laravel 9, but this way worked perfect, you can try it:
1- Create new Trait Class in project path app/Traits/IdAsUuidTrait.php (if you not found Traits folder create it, this is full code of this Class:
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait IdAsUuidTrait
{
public function initializeIdAsUuidTrait(): void
{
$this->keyType = 'string';
$this->id = Str::orderedUuid()->toString();
}
}
2- In any model you want to make id as UUID just call trait like this:
use App\Traits\IdAsUuidTrait;
class YourModelName extends Model
{
use IdAsUuidTrait;
...
That is it, now try to create, select, update any row in database by this model...

Laravel 4.1 eloquent model set appends dynamically

I am using Laravel 4.2.
I have two models: User and Video, both of these models are having one-to-many relationship i.e. User -> HasMany-> Video.
Recently, I got a requirement to display the list of users along with sum of file-size of total videos uploaded by each user and allow users to be order by the sum of file size ascending or descending.
I've made following changes in User model:
class User extends Eloquent {
protected $hidden = array('videosSum');
protected $appends = array('videos_size');
public function videosSum() {
return $this->hasOne('Video')
->selectRaw('sum(file_size) as sum, user_id')
->groupBy('user_id');
}
public function getVideosSizeAttribute()
{
// if relation is not loaded already, let's do it first
if ( ! array_key_exists('videos_size', $this->relations)){
$this->load('videosSum');
}
$related = $this->getRelation('videosSum');
return $this->attributes['videos_size'] = isset($related->sum) ? (int) $related->sum : 0;
}
}
And using like:
User::where('id', '!=', Auth::user()->id);
I am getting the desired result.
But the problem is, I don't want the videos_size attribute everywhere, where the User model gets called. I want to set it dynamically.
I tried User::$appends = ['videos_size'] but it gives protected property cannot be set outsize of class error.
I also tried to make a method in User model which set the $appends if called, but it is also not working.
Can anybody help me how to enable the appends property dynamically?
Laravel doesn't support this off the bat.
my friend and I wrote this extention:
Dynamically hide certain columns when returning an Eloquent object as JSON?
basically you have to override your models.php toArray() method as appended attributes get calculated when you ask for the model in json or array form.
you can add to the trait that's in that link and use it or just put these methods in your respective model class.
public static function getStaticAppends() {
return self::$_appends;
}
public static function setStaticAppends(array $value) {
self::$_appends = $value;
return self::$_appends;
}
public static function getDefaultAppends() {
return with(new static)->getAppends();
}
public function getAppends(){
return $this->appends;
}
public function toArray() {
if (self::getStaticAppends()) {
$this->appends = self::getStaticAppends();
}
return parent::toArray();
}

Global filtering - how to use global scope in Laravel Eloquent

I have a published filter that I use for my articles. Guests can only view published articles, logged in users can view and apply filter (?published=0/1):
public function scopePublishedFilter($query)
{
if(!Auth::check()) $query->where('published', '=', 1);
else
{
$published = Input::get('published');
if (isset($published)) $query->where('published', '=', $published);
}
return $query;
}
I apply this in my ArticlesController:
public function index()
{
return View::make('articles.index', [
'articles' => Article::with('owner')
->with('category')
->with('tags')
->publishedFilter()
->get()
]);
}
And on the article relationships:
public function articles()
{
return $this->hasMany('Article')->publishedFilter();
}
But ideally I would like to only define it in the Article model itself, since it's easy to forget to include this filter when implementing new features or views.
How can I make sure that all returned articles from the Article model are run through this filter before returned?
UPDATE: Just use this: https://github.com/jarektkaczyk/laravel-global-scope for global scopes in L5+
Better way is a bit too long to paste it and works like SoftDeleting thing in the core.
Read this if you want it http://softonsofa.com/laravel-how-to-define-and-use-eloquent-global-scopes/
Short way: you need global scope for this. And here's how you do it in 2 steps (squashed a bit):
1 Create a class PublishedScope that implements ScopeInterface
class PublishedScope implements ScopeInterface {
public function apply(Builder $builder)
{
$table = $builder->getModel()->getTable();
$builder->where($table.'.published', '=', 1);
$this->addWithDrafts($builder);
}
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$column = $builder->getModel()->getTable().'.published';
$bindingKey = 0;
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isPublishedConstraint($where, $column))
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
$this->removeBinding($query, $bindingKey);
}
// Check if where is either NULL or NOT NULL type,
// if that's the case, don't increment the key
// since there is no binding for these types
if ( ! in_array($where['type'], ['Null', 'NotNull'])) $bindingKey++;
}
}
protected function removeBinding(Builder $query, $key)
{
$bindings = $query->getRawBindings()['where'];
unset($bindings[$key]);
$query->setBindings($bindings);
}
protected function addWithDrafts(Builder $builder)
{
$builder->macro('withDrafts', function(Builder $builder)
{
$this->remove($builder);
return $builder;
});
}
2 Boot that class in your Eloquent model by calling static::addGlobalScope(new AbcScope)
// the model
public static function boot()
{
parent::boot();
static::addGlobalScope(new PublishedScope);
}
If I were you I would use published_at column and check it for null instead of = 1, but that's up to you.
edit remove method updated - thanks to #Leon for pointing out unexpected behaviour, when using this scope together with SoftDeletingTrait. The problem is a bit deeper:
when you use this one with SoftDeletingScope or another one, that utilizes NULL or NOT NULL constraint and this scope is not the first one used (yes, order of use statements matters here), remove method will not work as expected. It will not remove any binding or not the one, that it should.
you can use trait and add your method or filter thing in booting method check the following
http://laravel.com/docs/4.2/eloquent#global-scopes

Resources