Force create an eloquent model through a relationship - php

In a small project with Laravel 5.3 and Stripe, I am trying to force create a Subscription on a User through a hasOne relationship:
// User.php
public function subscription() {
return $this->hasOne('App\Subscription');
}
public function subscribe($data) {
return $this->subscription()->forceCreate(
// $data contains some guarded fields;
// create() will simply ignore them...
);
}
However, I get :
Call to undefined method Illuminate\Database\Query\Builder::forceCreate()
Even though forceCreate() is a valid Eloquent method.
Any ideas how I can simulate this behavior? Or should I just save a Subscription manually assigning each field? The complication is that certain fields should be kept guarded, e.g. stripe_id.
EDIT
My quick' n' dirty solution:
// User.php # subscribe($data)
return (new Subscription())
->forceFill([
'user_id' => $this->id,
// $data with sensitive guarded data
])
->save();
I'm sure there is a better way though!

The call to $this->subscriptions() returns an instance of HasMany class, not a Model.
Contrary to create and createMany, there's no forceCreate() implemented in HasOneOrMany class. And thus, there's no first-hand API to use in such situations.
You see? when you call $this->subscriptions()->create(), you're calling the create method on a HasMany instance, not a Model instance. The fact that Model class has a forceCreate method, has nothing to do with this.
You could use the getRelated() method to fetch the related model and then call forceCreate() or unguarded() on that:
public function subscribe($data) {
return $this->subscription()
->getRelated()
->forceCreate($data);
}
But there's a serious downside to this approach; it does not set the relation as we're fetching the model out of the relation. To work around it you might say:
public function subscribe($data)
{
// $data['user_id'] = $this->id;
// Or more generally:
$data[$this->courses()->getPlainForeignKey()] = $this->courses()->getParentKey();
return $this->courses()
->getRelated()
->forceCreate($data);
}
Ehh, seems too hacky, not my approach. I prefer unguarding the underlying model, calling create and then reguarding it. Something along the lines of:
public function subscribe($data)
{
$this->courses()->getRelated()->unguard();
$created = $this->courses()->create($data);
$this->courses()->getRelated()->reguard();
return $created;
}
This way, you don't have to deal with setting the foreign key by hand.
laravel/internals related discussion:
[PROPOSAL] Force create model through a relationship

Related

Call a Model method using Laravel Relationship

I'm currently trying to use Laravel Relationships to access my achievements Model using User model, I use this relationship code:
public function achievements()
{
return $this->hasMany('App\Models\User\Achievement');
}
I can easily make some eloquent queries, however I can't access any method that I created there, I can't access this method:
class Achievement extends Model
{
public function achievementsAvailableToClaim(): int
{
// Not an eloquent query
}
}
Using the following code:
Auth::user()->achievements()->achievementsAvailableToClaim();
I believe that I am using this Laravel function in the wrong way, because of that I tried something else without using relationship:
public function achievements()
{
return new \App\Models\User\Achievement;
}
But that would have a performance problem, because would I be creating a new class instance every time I use the achievements function inside user model?
What would be the right way of what I'm trying to do?
it's not working because your eloquent relationship is a hasMany so it return a collection. you can not call the related model function from a collection.
you can var dump it on tinker to understand more what i mean.
You can use laravel scopes.Like local scopes allow you to define common sets of constraints that you may easily re-use throughout your application.
In your case you use this like, Define scope in model:
public function scopeAchievementsAvailableToClaim()
{
return $query->where('achivement_avilable', true);
}
And you can use this like :
Auth::user()->achievements()->achievementsAvailableToClaim();

Laravel mocking model with persistence

I'm testing a job that receives a customer model. In the job I create the
customer in shopify and store that id on the customer model. Then I call a sendShopifyInvite that needs to be mocked (I don't want to send an email in my test).
My test looks like this:
/** #test */
public function a_shopify_customer_is_created_if_it_does_not_yet_exists()
{
$this->partialMock(User::class, function ($mock) {
$mock->shouldReceive('sendShopifyInvite')->once()->andReturn(new User());
});
$customer = app(User::class)->fill(
factory(User::class)->create([
'shopify_id' => null
])->toArray()
);
$this->assertNull($customer->shopify_id);
CreateCustomerJob::dispatchNow($customer);
$customer->refresh();
$this->assertNotNull($customer->shopify_id);
}
The problem is that I receive this error:
PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'jensssen_db.mockery_0__domain__customer__models__users' doesn't exist
Is it not possible to persist data in a mock object? Are there any other ways?
Your problem is due to this line. Since a partial mocks creates a new mock object and calls your original model through it, it will take that class basename.
return $this->table ?? Str::snake(Str::pluralStudly(class_basename($this)));
I can see two solutions, i don't feel like anyone is the perfect solutions, it will solve your problem. Firstly set your table hardcoded on the User.php model. This will avoid the class basename being called.
class User {
$protected table = 'users';
}
Another approach, i have been forced to make before (when you have a hard time mocking some classes). Is instead of mocking your User.php model, simply put the same logic in a service / proxy class and mock that instead.
class ShopifyService {
public function sendInvite(User $user)
{
...
}
}
In your User.php model now have.
public function sendShopifyInvite() {
resolve(ShopifyService::class)->sendInvite($this);
}
Now you are able to mock only the Shopify service and now not tinker with the inner workings of the Eloquent Model.
$this->partialMock(ShopifyService::class, function ($mock) {
$user = new User();
$mock->shouldReceive('sendInvite')->with($user)->once()->andReturn($user);
});

Laravel 5.8 newing up eloquent model and calling accessor in one line?

I have a contact_info_scopes table and one of the scopes is 'Default', which is likely to be the most common scope called, so I'm creating an accessor
public function getDefaultScopeIdAttribute()
{
return $this::where('contact_info_scope', 'Default')
->first()
->contact_info_scope_uuid;
}
to get the defaultScopeId and wondering how I can new up the ContactInfoScope model and access that in one line. I know I can new it up:
$contactInfoScope = new ContactInfoScope();
and then access it:
$contactInfoScope->defaultScopeId;
but I would like to do this in one line without having to store the class in a variable. Open to any other creative ways of tackling this as well since an accessor may not really be ideal here! I'd be fine with just creating a public function (not as an accessor), but would have the same issue of calling that in one line. Thanks :)
You should be able to call the model and chain the value if you return the instance in its constructor method
(new ContactInfoScope)->defaultScopeID
Not tried it in Laravel but works in plain ol PHP
//LARAVEL
//write on model ----------------------------------
protected $appends = ['image'];
public function getImageAttribute()
{
$this->school_iMage = \DB::table('school_profiles')->where('user_id',$this->id)->first();
$this->studend_iMage = \DB::table('student_admissions')->where('user_id',$this->id)->first();
return $this;
}
//call form anywhere blade and controller just like---------------------------
auth()->user()->image->school_iMage->cover_image;
or
User::find(1)->image->school_iMage->cover_image;
or
Auth::user()->image->school_iMage->cover_image;
// you can test
dd( User::find(1)->image);

Laravel belongs to many with additional methods

For followers relation on same User model I used belongsToMany()
public function followers() {
return $this->belongsToMany('App\User', 'followers', 'follow_id', 'user_id');
}
But since I am using this for chat list on load with vue I am on page load passing json_encode(auth()->user()->followers) which works as I needed.
But when I am lets say using only some columns like:
->select(['id', 'name', 'avatar']);
I have additional method for avatar:
public function image() {
return $this->avatar || 'some-default-image';
}
How can I pass that as well for each of many? Without withDefault method..
Try adding this in your User model
class User extends Model {
protected $appends = ['image'];
// other stuff..
}
this will forcefully inject your computed property ie. image in every User model instance but for it work you have to name your method (or create another) like getImageAttribute()and not simply image()
// getter for `image` property in user model object
public function getImageAttribute() {
return $this->avatar || 'some-default-image';
}
What you are looking for is the Accessor function:
Laravel Accessors and Mutators
Basically you define an accessor function in your model:
public function getAvatarAttribute($value)
{
return $value || 'some-default-image';
}
Then when you access the avatar property using ->avatar, the accessor function will get called and you will get the computed value.
====================================================================
The comment has words limit.
You have followers table where each follower is a User. You use relationship to filter all followers which are a group of Users. You wanted getInfo() to be called on each follower so that the additional data is appended to your JSON structure.
In that case, you don't need to filter through each follower and call getInfo() yourself. You use accessor method, put your code in getInfo() into an accessor method, and modify $appends array in your User model, then then JSON data will be automatically appended.
public function getUserInfoAttribute()
{
$userInfo = ... //use logic in your original getInfo method
return $userInfo;
}
Then you add user_info into your User model's $appends array:
protected $appends = ['user_info'];
This way, your user_info will be automatically included when your instance is serialized into JSON.
Like I said in the comment, you should check out:
Appending Values To JSON
for more information.
As to APIs , whether you are using Vue or React or anything, when passing JSON data for your frontend code to consume, you are basically creating apis. FYI:
Eloquent: API Resources

Laravel accessing collection attributes and model attributes from reusable view

I have a view that I'm trying to re-use for two different actions to display data from the database. For one of those actions, an Eloquent collection object is passed to the view, and data is retrieved with
#foreach($buildings as $key=>$value)
{!! $value->build_name !!}
Obviously 'build_name' is a column in the table. So far simple..
Now I need this same view to display data that requires a lot of processing and it's not possible to generate an eloquent statement to pass to the view.
In order to re-use the $value->build_name code, I'm assuming I have to still pass an object (model??) to the view.
I have a Building.php Model
class Building extends Model
{
protected $fillable =[
'buildingtype_id',
'build_name',
];
and I'm thinking I could just add public $build_name; to the Building model, but then I should also add a method to set and get the $build_name. So my Building Model will now look like..
class Building extends Model
{
public $build_name;
protected $fillable =[
'buildingtype_id',
'build_name',
];
public function getBuildName () {
return $this->build_name;
}
public function setBuildName ($name) {
$this->build_name = $name;
}
And I can just create the object myself in the controller...
If I do this, is {!! $value->build_name !!} still appropiate for the view? Or should I now be using {!! $value->getBuildName() !!}
Or am I missing a key concept somewhere? I'm still new to Laravel and OOP.
Edit
I just implemented this, and it's not working. If I add the public $build_name attribute to the model, getBuildName does not return anything, however if I remove public $build_name it does... (which would break my attempting to create that object manually)
When you declare public $build_name, this will override (or more precisely, reset) any other field with the same name in the model. So, you'll have to call setBuildName() setter method before you get it.
I just implemented this, and it's not working. If I add the public $build_name attribute to the model, getBuildName does not return anything
That's because you've called the getter method before the setter, so there is nothing (null) set in the public variable $build_name.
Although you haven't quite mentioned why exactly you want to reuse the Eloquent model, but you can achieve your desired purpose with a little tweak on the model's setter methods:
class Building extends Model
{
/* ... */
public function setBuildName ($name) {
$this->build_name = $name;
return $this;
}
}
Notice returning the current object ($this) in case you would want to chain multiple setter methods in one go.
e.g.:
$model->setBuildName('some name')->setBuildHeight(400);
UPDATE
You can use the default eloquent model to serve your purpose (hence, ridding you of making a duplicate class to achieve roughly the same effect).
Now suppose you have your model Building and would like to set it's attributes manually, then, the following operation on the model is still appropriate:
$building = new App\Building();
$building->build_name = 'Some Building Name'; // you can substitute this with your setter method as well
$building->build_height = 110; // assuming you have a column named `build_height` in your model's table
Note that the only difference in what you'd be doing here is:
You DON'T declare public variables at all in the Eloquent model.
You don't call Eloquent's save() method on the model to persist the manually set data (giving it a transient behavior).
The model now is totally eligible to be passed to your view and you can access it's attributes as you would with a regular model:
<!-- in your view -->
{{ $building->build_name }}
{{ $building->build_height }}
As an alternative approach to setting your arbitrary data, you can have a single setter which accepts an array of key value data to be stored in the model (e.g. ['build_name' => 'Building Name', 'build_height' => 110]):
//in class Building
public function setData($data){
$this->build_name = $data['build_name'];
$this->build_height = $data['build_height'];
}

Categories