Laravel - Set Default Value for Relation Models - php

I have a table accounts:
act_id,
act_name,
act_address
And I have a table addresses:
add_id,
add_street1,
<other fields you'd expect in an address table>
accounts.act_address is a foreign key to addresses.add_id. In Laravel, I have my Account model:
use LaravelBook\Ardent\Ardent;
use Illuminate\Database\Eloquent\SoftDeletingTrait;
class Account extends Ardent
{
use SoftDeletingTrait;
protected $table = 'accounts';
protected $primaryKey = 'act_id';
public static $rules = array
(
'act_name' => 'required|unique:accounts'
);
protected $fillable = array
(
'act_name'
);
public function address()
{
return $this->hasOne('Address', 'add_id', 'act_address');
}
}
As you can see, I have my one-to-one relationship setup here. (Of course, the Address model has a 'belongsTo' as well). This all works.
The thing is, the address foreign key is nullable, as accounts don't require addresses. So, if I try to access the Account->address when it doesn't have one, I'll get a 'trying to access property of non-object' error.
What I'd like to do is set Account->address to a new Address object (all fields empty), if the account record doesn't have one set.
What I've been able to do is either create a second method in the model:
public function getAddress()
{
return empty($this->address) ? new Address() : $this->address;
}
Or, add it on the fly:
if (empty($account->address))
$account->address = new Address();
The first solution is really close, but I'd really like to keep the functionality of accessing address as a property instead of a method.
So, my question is:
How can I have Account->address return new Address() if Account->address is empty/null?
Oh, and I tried overriding the $attributes like so:
protected $attributes = array
(
'address' => new Address()
);
But that throws an error.

Use accessor:
Edit: Since it is belongsTo not hasOne relation, it is a bit tricky - you can't associate a model to non-existing one, for the latter has no id:
public function getAddressAttribute()
{
if ( ! array_key_exists('address', $this->relations)) $this->load('address');
$address = ($this->getRelation('address')) ?: $this->getNewAddress();
return $address;
}
protected function getNewAddress()
{
$address = $this->address()->getRelated();
$this->setRelation('address', $address);
return $address;
}
However, now you need this:
$account->address->save();
$account->address()->associate($account->address);
which is not very convenient. You can alternatively save newly instantiated address in getNewAddress method, or override Account save method, to do the association automatically. Anyway for this relation I'm not sure if it makes sense to do it.. For hasOne it would play nice.
Below is the way how it should look like for hasOne relation:
public function getAddressAttribute()
{
if ( ! array_key_exists('address', $this->relations)) $this->load('address');
$address = ($this->getRelation('address')) ?: $this->getNewAddress();
return $address;
}
protected function getNewAddress()
{
$address = $this->address()->getRelated();
$this->associateNewAddress($address);
return $address;
}
protected function associateNewAddress($address)
{
$foreignKey = $this->address()->getPlainForeignKey();
$address->{$foreignKey} = $this->getKey();
$this->setRelation('address', $address);
}
You could do all this in single accessor, but this is the way it 'should' look like.

Related

Laravel Policy and Show Method with View Method logical Problem

I am using Policys and want to be sure that I am prevent data to be shown of other users.
In every Table I have the column 'user_id' and check if the current logged in user with his id the same with the data and his user_id.
In this specific case I have a table of Objects and Objektverwaltung where the objekt_id is given as foreign key.
I want to use my policy to be sure that just the data for the given object was shown in objektverwaltung where the foreign key 'objekt_id' is given.
ObjektVerwaltung Controller with the show method:
public function show($objektvwId) {
$objektId = ObjektVerwaltung::with('Objekt')->find($objektvwId);
$this->authorize('view', $objektId);
$objekte = ObjektVerwaltung::where('objekt_id',$objektvwId)->get();
return view('objekte.verwaltung', compact('objekte'));
}
Policy:
public function view(User $user, ObjektVerwaltung $objektVerwaltung)
{
return $objektVerwaltung->user_id === $user->id;
}
Models:
class ObjektVerwaltung extends Model
{
use HasFactory;
protected $table = 'objekte_verwaltungens';
protected $fillable = ['user_id','objekt_id','key', 'value'];
public function Objekt() {
return $this->belongsTo(Objekt::class);
}
}
class Objekt extends Model
{
use HasFactory;
protected $table = 'objekts';
protected $fillable = ['name','strasse', 'hausnummer', 'plz', 'ort', 'user_id'];
public function Mieter() {
return $this->hasMany(Mieter::class);
}
public function Company() {
return $this->belongTo(Company::class);
}
public function Objektverwaltung() {
return $this->hasMany(ObjektVerwaltung::class);
}
}
I learned that I can easily use find() as method for the Models to validate data. But in this specific case I have to check for the objekt_id (foreign key in objektverwaltung) and not for the ID and because of that I cant use find(). But if I use where or another method I cant use my policy and always getting unauthorized.
I tried to use the with method on the model but maybe there is a better way to my problem. I strongly believe.
Thanks!
This could be solution, but I am getting always "Unauthorized" and do not get to the policy: $objekt= ObjektVerwaltung::where('objekt_id', $objektId)->get(); $this->authorize('view', $objekt);
I solved this issue. I had to use my ObjektPolicy, because I am using the objekt_id Key.

Using hasone function with constructed key

I'm new to Laravel (but not PHP).
I have two database files (which cannot be modified).
One file (salesorderlines) has two keys ordn55 and ordl55 which are an Id and a sequence number. The other file (certificate) combines these two keys with a '-' between them and the leading zeros on the sequence number.
I can construct the key to this file using the following code:
$this->ordn55.'-'. sprintf('%03s', $this->ordl55);
which works fine.
However I can't do this with a hasone() function call.
Is this possible to do with Laravel Eloquent? If not, what if the best way to do it.
Here is more detailed code for this:
SalesOrderLine model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SalesOrderLine extends Model
{
protected $connection = 'ibmi';
public $incrementing = false;
protected $table = 'ault2f1.oep55';
protected $primaryKey = 'ordn55';
protected $appends = ['combinedkey'];
public $timestamps = false;
public function salesorder()
{
return $this->hasOne(SalesOrderHeader::class, 'ordn40', 'ordn55')->where('cono40', 'AM');
}
public function item()
{
return $this->hasOne(ItemMaster::class, 'pnum35', 'catn55')->where('cono35', 'AM');
}
public function certificate()
{
return $this->hasOne(Certificate::class, 'order_no', 'combinedkey')->where('cono70', 'AM');
}
public function getCombinedkeyAttribute()
{
return $this->ordn55.'-'. sprintf('%03s', $this->ordl55);
}
}
You can see that I have tried to achieve this using a combinedkey property that I have added to the $appends array but this doesn't seem to work.
Here is the code to run the query for this
$salesorder = SalesOrderHeader::query()
->with('salesorderlines', 'salesorderlines.item')
->with('salesorderlines', 'salesorderlines.certificate')
->join('ault1f1.slp05', 'ault1f1.slp05.cusn05', 'cusn40')
->where('cono40', 'AM')
->where('cono05', 'AM')
->where('ordn40', $id)
->first();
The item section works correctly as it doesn't need a constructed key for the foreign key like the Certificate does.
Hopefully this makes sense.
Thanks in advance for any help

Accessor for relation objects in Laravel

this is my Category model:
/**
* #var array
*/
protected $guarded = ['id'];
public function media()
{
return $this->belongsTo(Media::class);
}
public function getMediaAttribute()
{
return 'Foo';
return ( ! is_null($this->media))
? $this->media
: '/products/default/thumb.jpg';
}
and when i call it in route for get all object like this:
return \App\Category::with('media')->get();
it seems accessor not work and i can't get 'Foo' in category's media object
You can use withDefault():
public function media()
{
return $this->belongsTo(Media::class)
->withDefault(['url' => '/products/default/thumb.jpg']);
}
When there is no result, it returns a Media instance with the given attributes.
that's not how laravel accessors work
if you created it like
public function getMediaAttribute()
{
return 'Foo';
return ( ! is_null($this->media))
? $this->media
: '/products/default/thumb.jpg';
}
then you will access it like:
return \App\Category::first()->media;
it will work as 'additional field' which can be manipulated in various ways for your model, in this case for the model category
more info on that:
https://laravel.com/docs/5.6/eloquent-mutators#defining-an-accessor
also as mentioned in the comment under your question, accessor with the same name as the relationship will override original field with given name, I'm not 100% sure the last part about overriding, that's how it works with mutators, I think it's same with accessors

Laravel observer for pivot table

I've got a observer that has a update method:
ObserverServiceProvider.php
public function boot()
{
Relation::observe(RelationObserver::class);
}
RelationObserver.php
public function updated(Relation $relation)
{
$this->cache->tags(Relation::class)->flush();
}
So when I update a relation in my controller:
public function update(Request $request, Relation $relation)
{
$relation->update($request->all()));
return back();
}
Everything is working as expected. But now I've got a pivot table. A relation belongsToMany products.
So now my controller method looks like this:
public function update(Request $request, Relation $relation)
{
if(empty($request->products)) {
$relation->products()->detach();
} else {
$relation->products()->sync(collect($request->products)->pluck('id'));
}
$relation->update($request->all());
return back();
}
The problem is that the observer is not triggered anymore if I only add or remove products.
How can I trigger the observer when the pivot table updates aswel?
Thanks
As you already know, Laravel doesn't actually retrieve the models nor call save/update on any of the models when calling sync() thus no event's are created by default. But I came up with some alternative solutions for your problem.
1 - To add some extra functionality to the sync() method:
If you dive deeper into the belongsToMany functionality you will see that it tries to guess some of the variable names and returns a BelongsToMany object. Easiest way would be to make your relationship function to simply return a custom BelongsToMany object yourself:
public function products() {
// Product::class is by default the 1. argument in ->belongsToMany calll
$instance = $this->newRelatedInstance(Product::class);
return new BelongsToManySpecial(
$instance->newQuery(),
$this,
$this->joiningTable(Product::class), // By default the 2. argument
$this->getForeignKey(), // By default the 3. argument
$instance->getForeignKey(), // By default the 4. argument
null // By default the 5. argument
);
}
Or alternatively copy the whole function, rename it and make it return the BelongsToManySpecial class. Or omit all the variables and perhaps simply return new BelongsToManyProducts class and resolve all the BelongsToMany varialbes in the __construct... I think you got the idea.
Make the BelongsToManySpecial class extend the original BelongsToMany class and write a sync function to the BelongsToManySpecial class.
public function sync($ids, $detaching = true) {
// Call the parent class for default functionality
$changes = parent::sync($ids, $detaching);
// $changes = [ 'attached' => [...], 'detached' => [...], 'updated' => [...] ]
// Add your functionality
// Here you have access to everything the BelongsToMany function has access and also know what changes the sync function made.
// Return the original response
return $changes
}
Alternatively override the detach and attachNew functions for similar results.
protected function attachNew(array $records, array $current, $touch = true) {
$result = parent::attachNew($records, $current, $touch);
// Your functionality
return $result;
}
public function detach($ids = null, $touch = true)
$result = parent::detach($ids, $touch);
// Your functionality
return $result;
}
If you want to dig deeper and want to understand what's going on under the hood then analyze the Illuminate\Database\Eloquent\Concerns\HasRelationship trait - specifically the belongsToMany relationship function and the BelongsToMany class itself.
2 - Create a trait called BelongsToManySyncEvents which doesn't do much more than returns your special BelongsToMany class
trait BelongsToManySyncEvents {
public function belongsToMany($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) {
if (is_null($relation)) {
$relation = $this->guessBelongsToManyRelation();
}
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$relatedKey = $relatedKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
return new BelongsToManyWithSyncEvents(
$instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
);
}
}
Create the BelongsToManyWithSyncEvents class:
class BelongsToManyWithSyncEvents extends BelongsToMany {
public function sync($ids, $detaching = true) {
$changes = parent::sync($ids, $detaching);
// Do your own magic. For example using these variables if needed:
// $this->get() - returns an array of objects given with the sync method
// $this->parent - Object they got attached to
// Maybe call some function on the parent if it exists?
return $changes;
}
}
Now add the trait to your class.
3 - Combine the previous solutions and add this functionality to every Model that you have in a BaseModel class etc. For examples make them check and call some method in case it is defined...
$functionName = 'on' . $this->foreignKey . 'Sync';
if(method_exists($this->parent), $functionName) {
$this->parent->$functionName($changes);
}
4 - Create a service
Inside that service create a function that you must always call instead of the default sync(). Perhaps call it something attachAndDetachProducts(...) and add your events or functionality
As I didn't have that much information about your classes and relationships you can probably choose better class names than I provided. But if your use case for now is simply to clear cache then I think you can make use of some of the provided solutions.
When I search about this topic, it came as the first result.
However, for newer Laravel version you can just make a "Pivot" model class for that.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PostTag extends Pivot
{
protected $table = 'post_tag';
public $timestamps = null;
}
For the related model
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->using(PostTag::class);
}
and you have to put your declare your observer in EventServiceProvider
as stated in Laravel Docs
PostTag::observe(PostTagObserver::class);
Reference: Observe pivot tables in Laravel
Just add:
public $afterCommit = true;
at the beginning of the observer class.. It will wait until the transactions are done, then performs your sync which should then work fine..
Please check Laravel's documentation for that.
It seems this solutions was just added in Laravel 8.

Add a custom attribute to a Laravel / Eloquent model on load?

I'd like to be able to add a custom attribute/property to an Laravel/Eloquent model when it is loaded, similar to how that might be achieved with RedBean's $model->open() method.
For instance, at the moment, in my controller I have:
public function index()
{
$sessions = EventSession::all();
foreach ($sessions as $i => $session) {
$sessions[$i]->available = $session->getAvailability();
}
return $sessions;
}
It would be nice to be able to omit the loop and have the 'available' attribute already set and populated.
I've tried using some of the model events described in the documentation to attach this property when the object loads, but without success so far.
Notes:
'available' is not a field in the underlying table.
$sessions is being returned as a JSON object as part of an API, and therefore calling something like $session->available() in a template isn't an option
The problem is caused by the fact that the Model's toArray() method ignores any accessors which do not directly relate to a column in the underlying table.
As Taylor Otwell mentioned here, "This is intentional and for performance reasons." However there is an easy way to achieve this:
class EventSession extends Eloquent {
protected $table = 'sessions';
protected $appends = array('availability');
public function getAvailabilityAttribute()
{
return $this->calculateAvailability();
}
}
Any attributes listed in the $appends property will automatically be included in the array or JSON form of the model, provided that you've added the appropriate accessor.
Old answer (for Laravel versions < 4.08):
The best solution that I've found is to override the toArray() method and either explicity set the attribute:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
$array['upper'] = $this->upper;
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
or, if you have lots of custom accessors, loop through them all and apply them:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
foreach ($this->getMutatedAttributes() as $key)
{
if ( ! array_key_exists($key, $array)) {
$array[$key] = $this->{$key};
}
}
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
The last thing on the Laravel Eloquent doc page is:
protected $appends = array('is_admin');
That can be used automatically to add new accessors to the model without any additional work like modifying methods like ::toArray().
Just create getFooBarAttribute(...) accessor and add the foo_bar to $appends array.
If you rename your getAvailability() method to getAvailableAttribute() your method becomes an accessor and you'll be able to read it using ->available straight on your model.
Docs: https://laravel.com/docs/5.4/eloquent-mutators#accessors-and-mutators
EDIT: Since your attribute is "virtual", it is not included by default in the JSON representation of your object.
But I found this: Custom model accessors not processed when ->toJson() called?
In order to force your attribute to be returned in the array, add it as a key to the $attributes array.
class User extends Eloquent {
protected $attributes = array(
'ZipCode' => '',
);
public function getZipCodeAttribute()
{
return ....
}
}
I didn't test it, but should be pretty trivial for you to try in your current setup.
I had something simular:
I have an attribute picture in my model, this contains the location of the file in the Storage folder.
The image must be returned base64 encoded
//Add extra attribute
protected $attributes = ['picture_data'];
//Make it available in the json response
protected $appends = ['picture_data'];
//implement the attribute
public function getPictureDataAttribute()
{
$file = Storage::get($this->picture);
$type = Storage::mimeType($this->picture);
return "data:" . $type . ";base64," . base64_encode($file);
}
Step 1: Define attributes in $appends
Step 2: Define accessor for that attributes.
Example:
<?php
...
class Movie extends Model{
protected $appends = ['cover'];
//define accessor
public function getCoverAttribute()
{
return json_decode($this->InJson)->cover;
}
you can use setAttribute function in Model to add a custom attribute
Let say you have 2 columns named first_name and last_name in your users table and you want to retrieve full name. you can achieve with the following code :
class User extends Eloquent {
public function getFullNameAttribute()
{
return $this->first_name.' '.$this->last_name;
}
}
now you can get full name as:
$user = User::find(1);
$user->full_name;
In my subscription model, I need to know the subscription is paused or not.
here is how I did it
public function getIsPausedAttribute() {
$isPaused = false;
if (!$this->is_active) {
$isPaused = true;
}
}
then in the view template,I can use
$subscription->is_paused to get the result.
The getIsPausedAttribute is the format to set a custom attribute,
and uses is_paused to get or use the attribute in your view.
in my case, creating an empty column and setting its accessor worked fine.
my accessor filling user's age from dob column. toArray() function worked too.
public function getAgeAttribute()
{
return Carbon::createFromFormat('Y-m-d', $this->attributes['dateofbirth'])->age;
}

Categories