Laravel - hasOne relation based on model attribute - php

I have a problem, I need to make a relation based on an attribute from the model$this->type, my code is as follows:
class Parent extends Model {
protected $attributes = [
'type' => self::TYPE_A
];
public function child()
{
return match ($this->type) {
self::TYPE_A => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_A),
self::TYPE_B => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_B),
self::TYPE_C,
self::TYPE_D => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_C),
self::TYPE_E,
self::TYPE_F => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_D)
};
}
public function children()
{
return $this->hasMany(Cousin::class);
}
}
If I do this inside artisan tinker, I get the child relation correctly:
$parent = Parent::first();
$parent->child;
However if my queues execute the code the child relation is returned as null.
If I put logger($this) in the first line of the relation I get an empty model when inside queues:
[2021-09-29 23:51:59] local.DEBUG: {"type":1}
Any idea on how to solve this?

Apparently it is not possible to use the relationship in this way.
The relationship I want uses composite keys and Eloquent doesn't support it.
They created a package to enable this to happen: https://github.com/topclaudy/compoships
But I don't want to add any more packages to my application as it is already too big.
My solution was to add one more column in the cousins table to know which one to use:
public function child() {
return $this->hasOne(Cousin::class)
->where('main', true);
}

protected $attributes = [
'type'
];
public function getTypeAttribute() {
return $this->hasOne(Cousin::class)
->where('main', true);
}
Then call it anywhere
$parent->type
Or you can do it as follow
public function typeMainTrue() {
return $this->hasOne(Cousin::class)
->where('main', true);
}
Then call it anywhere
$parent->typeMainTrue

Related

How to iterate and add a "random" key/value to an existing object using hasMany

Using the with method, I do not see anything near, on the docs, as to what I want to accomplish. Possible?
// bars is a hasMany() association
public function getFoo($id, $fooId)
{
return $this
->where('id', $id)
->where('fooId', $fooId)
->with([
'one',
'two',
'bars' => function($queries) {
foreach ($queries as $key => $query) {
$queries[$key]['extraKey'] = 'extraValue'; // extrakey can be any name I want it to be.
// $query['extraKey'] = "extraValue";
}
}
])
->first();
}
I'm following Vlad's answer how to loop and modify but I'm not seeing extraKey in the returned data.
Do I have perform this action in the controller? Seems messy if that's the only way. I thought I could so these actions within the model itself.
I expect to see:
foo->bars[0]->extrakey;
You could make use of accessors in your Bar model:
class Bar extends Model
{
// ...
public function getExtraKeyAttribute()
{
return 'extraValue';
}
// ...
}
Additionally, if you want to include this value in the toArray output, add it the protected $appends property of your model:
class Bar extends Model
{
protected $appends = ['extra_key'];

Laravel getAttribute not appended to the model

I have added a accessor to my User.php model getArticlesCountAttribute
protected $appends = ['articles_count'];
public function getArticlesCountAttribute()
{
return (int) $this->articles()->count();
}
but when i access it from my laravel blade views articles count database query runs every-time i use the accessor in my view
For example: if i have Auth::user()->articles_count 3 times in my blade view it will run articles count query 3 times.
Shouldn't it be appended at the start and query should only run once no matter how many times i reference it in my views?
I think you should not cache or store it. It is still a model and a model should not have this kind of implementations.
You should restructure your view and pass the result of the count from the controller to the view.
public function myAction()
{
return view('myview', [
'articleCount' => Auth::user()->articles_count
]);
}
And in the view have than once the result in the variable $articleCount.
Try this one code
protected $hidden = ['articles_count'];
public function getArticlesCountAttribute()
{
return (int) $this->articles()->count();
}
You just have to access the attribute, not the method:
protected $appends = ['articles_count'];
public function getArticlesCountAttribute()
{
return (int) $this->articles->count();
}
This way if articles is not loaded it will be the first time :)
You could cache it, for example:
protected $articlesCounted;
public function getArticlesCountAttribute()
{
if (is_null($this->articlesCounted)) {
return $this->articlesCounted = $this->articles()->count();
}
return $this->articlesCounted;
}

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.

Phalcon ORM: hasOne(self)

I've got a self referencing table in my db and would like to pull it as an object using Phalcon ORM. The hasOne() relationship works when joining to other tables but doesnt seem to work when trying to reference itself. When trying to var_dump($treeNode->TreeNodes) it returns nothing. Inspecting the object just returns 'Cannot evaluate expression' using XDebug.
Does anyone know how to do this in Phalcon?
public function organisationAction()
{
$organisation = new Organisations();
$organisation->setConnectionService(Registry::setConnection(Connections::UK_Connection));
$organisation = $organisation->findFirst(123);
$treeNodes = $organisation->TreeNodes;
foreach($treeNodes as $treeNode){
var_dump($treeNode->TreeNodes);
}
}
class TreeNodes extends Model
{
public $node_id;
public $tree_id;
public $tree_parent_node_id;
public $tree_level_id;
public $node_desc;
public function getSource()
{
return "TreeNodes";
}
public function initialize()
{
$this->setSource("TreeNodes");
$this->hasOne(
'tree_parent_node_id',
'TreeNodes',
'node_id',
array(
'reusable' => true
)
);
}
}
An easy way to achieve this is by defining an additional relationship
$this->belongsTo('tree_parent_node_id', // which column
'TreeNodes', // referenced table
'id', // referenced table column
['alias' => 'parentNode']);
To avoid confusion you can give this relation an alias. ['alias' => 'parentNode']
Now you should be able to access the related parent node by using
$treeNode->getRelated('parentNode');
More information about Phalcon aliases.

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