Overriding cast type on pivot model - php

I have below model:
Document.php
/**
* Get all of the documents field content.
*
* #return Illuminate\Database\Eloquent\Model
*/
public function fields()
{
return $this->morphToMany(Field::class, 'fieldable')
->using('App\FieldablePivot')
->withPivot(['content', 'type'])
->withTimestamps();
}
That's using a Pivot model called FieldablePivot, where I need to access return the content column.
This is my FieldablePivot, where I have overwritten the getCastType method:
protected function getCastType($key)
{
if ($key == 'content' && !empty($this->type)) {
return $this->type;
}
return parent::getCastType($key);
}
Below is the rows of fieldables:
id | name | content | type |
---------------------------------------------
1 | field_one | [somearray] | array |
2 | field_two | somestring | string |
However, when I access a document and it's fields to get the content of a field, like:
#foreach ($document->fields as $field)
{{dd($field->pivot->content)}}
#endforeach
It returns the first one as a string (even though the type is of array):
"[somearray]"

You will have to override the hasCast function as well to make sure Eloquent knows there is a cast for this column.
public function hasCast($key, $types = null)
{
if ($key === 'content') {
return true;
}
return parent::hasCast($key, $types);
}
If you don't, and the field is not in the $casts array, Eloquent will not detect that the attribute has a cast.

Related

PHP - Saving relationship - shared and unique references

I have 4 models:
Stream
Field
Document
Result
I then have below relationships defined:
Stream.php:
public function fields()
{
return $this->hasMany(Field::class);
}
public function documents()
{
return $this->hasMany(Document::class);
}
Field.php:
public function stream()
{
return $this->belongsTo(Stream::class);
}
public function result()
{
return $this->hasOne(Result::class);
}
Document.php:
public function stream()
{
return $this->belongsTo(Stream::class);
}
Result.php:
public function field()
{
return $this->hasOne(Field::class);
}
Now, my users can upload documents to a stream, and my users can create many fields on a stream.
When a document is uploaded to a stream,for each field defined, the document content will be parsed according to some logic I have created.
The end result of this parsing, should be saved to the database (results).
I can do this now, by:
$document = Document::find(1);
foreach($stream->fields as $field)
{
$content = myParseFunction($document->content);
$field->save(['content' => $content]);
}
This will create a result for each field on the stream. Like, if my Stream had 2 fields:
results table:
id | field_id | content
1 | 1 | Lorem ipsum
2 | 2 | Another ipsum
Whenever I upload a new document however, the result will be overwritten.
As each uploaded documents content is unique, how can I save the result for the field and specific document.
Like, if my Stream still have 2 fields, but I upload 2 documents:
results table:
id | field_id | document_id | content
1 | 1 | 1 | Lorem ipsum
2 | 2 | 1 | Another ipsum
3 | 1 | 2 | Another unique content
4 | 2 | 2 | Yet another unique content
I think the result is also determined by the document, as you are parsing a document for a for a field.
I would also link the documents to theuse the Result table ans store the results there.
One result would be determinded by the document id and the field id.
Result
public function fields()
{
return $this->hasMany(Field::class);
}
public function documents()
{
return $this->hasMany(Document::class);
}
Document
public function results()
{
return $this->hasMany(Result::class);
}
Field
public function stream()
{
return $this->belongsTo(Stream::class);
}
public function result()
{
return $this->hasOne(Result::class);
}
And then:
$document = Document::find(1);
$result = new Result();
foreach($stream->fields as $field)
{
$content = myParseFunction($document->content);
$result->save(['field_id' => $field->id, 'document_id'=>$document->id, 'content' => $content]);
}

Laravel ManyToMany relationship on same model

I'm trying to make a bidirectional ManyToMany relationship on my Tag model, but I ran into this "issue".
My model looks like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $table = 'tags';
public $timestamps = false;
public function tags()
{
return $this->belongsToMany(Tag::class, 'tag_tag', 'tag_one_id', 'tag_two_id');
}
}
So at the moment let's say I have Tag1 and Tag2 in my tags table, and then I'll relate the Tag2 with the Tag1.
Now my pivot table will look like this:
+----+------------+------------+
| id | tag_one_id | tag_two_id |
+----+------------+------------+
| 1 | 1 | 2 |
+----+------------+------------+
When I try this code:
$tag = Tag::find(1);
$tag->tags()->get();
I get the Tag2 instance, and it is correct.
But when I try to run this code:
$tag = Tag::find(2);
$tag->tags()->get();
I would like to receive the Tag1 instance, but I don't.
Is it possible to get it done with Laravel default Eloquent using just one method on the model?
I found a solution and I solved it like this.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
/**
* #inheritdoc
*
* #var string
*/
protected $table = 'tags';
/**
* #inheritdoc
*
* #var bool
*/
public $timestamps = false;
/*
|--------------------------------------------------------------------------
| RELATIONS
|--------------------------------------------------------------------------
*/
/**
* Every tag can contain many related tags (TagOne has many TagTwo).
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
protected function tagsOneTwo()
{
return $this->belongsToMany(Tag::class, 'tag_tag', 'tag_one_id', 'tag_two_id');
}
/**
* Every tag can contain many related tags (TagTwo has many TagOne).
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
protected function tagsTwoOne()
{
return $this->belongsToMany(Tag::class, 'tag_tag', 'tag_two_id', 'tag_one_id');
}
/**
* This method returns a collection with all the tags related with this tag.
* It is not a real relation, but emulates it.
*
* #return \Illuminate\Database\Eloquent\Collection
*/
public function tags()
{
return $this->tagsOneTwo()->get()->merge($this->tagsTwoOne()->get())->unique('id');
}
/*
|--------------------------------------------------------------------------
| FUNCTIONS
|--------------------------------------------------------------------------
*/
/**
* Function to relate two tags together.
*
* #param Tag $tag
* #return void;
*/
public function attach(Tag $tag)
{
if ($this->tags()->contains('id', $tag->getKey())) {
return;
}
$this->tagsOneTwo()->attach($tag->getKey());
}
/**
* Function to un-relate two tags.
*
* #param Tag $tag
* #return void;
*/
public function detach(Tag $tag)
{
if ($this->tags()->contains('id', $tag->getKey())) {
// Detach the relationship in both ways.
$this->tagsOneTwo()->detach($tag->getKey());
$this->tagsTwoOne()->detach($tag->getKey());
}
}
/*
|--------------------------------------------------------------------------
| ACCESORS
|--------------------------------------------------------------------------
*/
/**
* Let access the related tags like if it was preloaded ($tag->tags).
*
* #return mixed
*/
public function getTagsAttribute()
{
if (! array_key_exists('tags', $this->relations)) {
$this->setRelation('tags', $this->tags());
};
return $this->getRelation('tags');
}
}
Why it's not working?
It's not working because in the relationship you added in Tag model is defined to work for one way. But not the reverse way.
It would work if we could have defined two method called tags() as below:
public function tags()
{
return $this->belongsToMany(Tag::class, 'tag_tag', 'tag_one_id', 'tag_two_id');
}
//and
public function tags()
{
return $this->belongsToMany(Tag::class, 'tag_tag', 'tag_two_id', 'tag_one_id');
}
Unfortunately, it's not possible.
So, what can be possible solution
One possible solution can be, don't touch the relationship. Instead if you somehow can manage to insert two relationship for these relationship then it will work.
For example:
+----+------------+------------+
| id | tag_one_id | tag_two_id |
+----+------------+------------+
| 1 | 1 | 2 |
+----+------------+------------+
| 1 | 2 | 1 |
+----+------------+------------+
This is the solution coming out of my mind right now. There might be a better solution too.
It's possible. You would have to pass a parameter to the tags() method and modify the primary/foreign key fields on the relationship using that parameter. That might be an total pain to have to deal with though and it would almost certainly be less of a headache to just make a second relationship method. It would just end up looking like this:
public function tags($tag1 = 'tag_one_id', $tag2 = 'tag_two_id')
{
return $this->belongsToMany(Tag::class, 'tag_tag', $tag1, $tag2);
}
and then you would just have to modify the values when you need Tag::find(2)->tags('tag_two_id', 'tag_one_id')
This can be eager loaded as described here: https://laracasts.com/discuss/channels/general-discussion/eager-load-with-parameters
Your use case might be a lot more complicated than your post suggests, which might make this more reasonable. As it is I would consider a few of the other options.

Currency formatting in laravel

Could you help me with formatting amount figures in the Laravel blade based on model currency, please? Value is stored as double and currency as char(3) 'usd','eur','czk','huf'...
The projects has to be displayed in given currency format:
| 1 | project A | $1,250.00 |
| 2 | project B | €6,000.00 |
| 3 | project C | 1,250.00CZK |
PHP money_format uses locale where I want to use project specific currency in each row or each project page.
You can implement your own transformers, something like this:
In your controller
public function getRows(Request $request, CurrencyTransformer $transformer) {
// get data
$rows = Model::where('some', 'condition')->get()->toArray();
$data = $transformer->transformCollection($rows);
return view('some.view', compact('data'));
}
Create a abstract transformer class
abstract class Transformer
{
public function transformCollection(array $items)
{
return array_map([$this, 'transform'], $items);
}
public abstract function transform($item);
}
create a currency transformer
class CurrencyTransformer extends Transformer
{
public function transform($item) {
switch($item['currency']) {
case "usd":
$item['value'] = "$" . $item['value'];
break;
...
}
return $item;
}
}

Laravel parent / child relationship on the same model

The Setup And Dummy Data
I have a simple model called Category, which has the following schema:
|----------------------------------------------|
| cat_id | cat_name | parent_id |
|----------------------------------------------|
| 1 | Home | 0 |
|----------------------------------------------|
| 2 | Products | 1 |
|----------------------------------------------|
| 3 | Services | 1 |
|----------------------------------------------|
| 4 | Product A | 2 |
|----------------------------------------------|
| 5 | Product B | 2 |
|----------------------------------------------|
The Desired Output
So you can see that we would get a very straight forward hierarchy as follows:
Home
- Products
- Product A
- Product B
- Services
The Issue
I'm trying to map this relationship in Laravel 4.2, so that I can query a model and get its parent (it will always have a parent), and child categories if they exist.
I've defined the relationship in the Category model using:
public function children()
{
return $this->hasMany('Category', 'parent_id', 'cat_id');
}
public function parent()
{
return $this->belongsTo('Category', 'parent_id');
}
The Problem
I can get the parent name working, using
$category = Category::findOrFail($id);
return $category->parent->cat_name;
However, I don't understand how to get the child objects.
I've tried:
$category = Category::findOrFail($id);
$children = $category->children();
But when I dd($children) it doesn't output what I'd expect.
Calling the relationship function (->children()) will return an instance of the relation class. You either need to call then get() or just use the property:
$children = $category->children()->get();
// or
$children = $category->children;
Further explanation
Actually children() and children are something pretty different. children() just calls the method you defined for your relationship. The method returns an object of HasMany. You can use this to apply further query methods. For example:
$category->children()->orderBy('firstname')->get();
Now accessing the property children works differently. You never defined it, so Laravel does some magic in the background.
Let's have a look at Illuminate\Database\Eloquent\Model:
public function __get($key)
{
return $this->getAttribute($key);
}
The __get function is called when you try to access a property on a PHP object that doesn't actually exist.
public function getAttribute($key)
{
$inAttributes = array_key_exists($key, $this->attributes);
// If the key references an attribute, we can just go ahead and return the
// plain attribute value from the model. This allows every attribute to
// be dynamically accessed through the _get method without accessors.
if ($inAttributes || $this->hasGetMutator($key))
{
return $this->getAttributeValue($key);
}
// If the key already exists in the relationships array, it just means the
// relationship has already been loaded, so we'll just return it out of
// here because there is no need to query within the relations twice.
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
// If the "attribute" exists as a method on the model, we will just assume
// it is a relationship and will load and return results from the query
// and hydrate the relationship's value on the "relationships" array.
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
Then in getAttribute first is some code that checks for "normal" attributes and returns then. And finally, at the end of the method, if there's a relation method defined getRelationshipFromMethod is called.
It will then retrieve the result of the relationship and return that.
Set this in model and try :
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
public function grandchildren()
{
return $this->children()->with('grandchildren');
}

MySQL nested resources (pivot tables) permission to view/update/manage

users transactions tasks
+----+--------+ +----+---------------+ +----+--------+
| id | name | | id | name | | id | name |
+----+--------+ +----+---------------+ +----+--------+
| 1 | User 1 | | 1 | Transaction 1 | | 1 | Task 1 |
| 2 | User 2 | | 2 | Transaction 2 | | 2 | Task 2 |
+----+--------+ +----+---------------+ +----+--------+
templates transaction_user task_transaction
+----+---------------+ +---------+----------------+ +---------+----------------+
| id | name | | user_id | transaction_id | | task_id | transaction_id |
+----+---------------+ +---------+----------------+ +---------+----------------+
| 1 | Template 1 | | 1 | 1 | | 1 | 1 |
| 2 | Template 2 | | 2 | 2 | +---------+----------------+
+----+---------------+ +---------+----------------+
task_template
+---------+-------------+
| task_id | template_id |
+---------+-------------+
| 2 | 2 |
+---------+-------------+
Motive:
If there is a logged in user, say user with the ID 1, and he/she wants to see a task (say task with the ID 1) then i want to make sure that the task with ID 1 Belongs to the user before i let him view it. Also i need someway to show user all tasks that belong to him. Task is just one model.. i need to handle this for all models. I have shared my code below, am i trying too hard?
I may have omitted some details here so please feel free to ask questions.
Thanks.
Code
<?php namespace SomeProject\Repositories;
use User;
use Account;
use Task;
use Document;
use Transaction;
use Property;
use DB;
use Respond;
abstract class DbRepository
{
/**
* The many to many relationships are handeled using pivot tables
* We will use this array to figure out relationships and then get
* a particular resource's owner / account
*/
public $pivot_models = array(
'Task' => array(
'Transaction' => 'task_transaction'
),
'Transaction' => array(
'User' => 'transaction_user'
),
'Document' => array(
'Property' => 'document_property',
'Task' => 'document_task',
'Message' => 'document_message'
)
);
public $entity_ids;
public function getOwnersByEntity(array $ids, $entity)
{
$this->entity_ids = [];
$user_ids = [];
$entity = ucfirst(strtolower($entity)); // arrays keys are case sensitive
if( $this->getPivotIds($ids, $entity) )
{
foreach ($this->entity_ids as $entity_name => $entity_ids_arr)
{
$entity_name_lowercase = strtolower($entity_name);
if($entity_name_lowercase != 'user')
{
$user_ids_from_entity = $entity_name::whereIn('id', $entity_ids_arr)
->lists('user_id');
}
else
{
// We already have the IDs if the entity is User
$user_ids_from_entity = $entity_ids_arr;
}
array_push($user_ids, $user_ids_from_entity);
}
$merged_user_ids = call_user_func_array('array_merge', $user_ids);
return array_unique($merged_user_ids);
}
else
{
return $entity::whereIn('id', $ids)->lists('user_id');
}
}
public function getPivotIds(array $ids, $entity)
{
$entity_lowercase = strtolower($entity);
if( array_key_exists($entity, $this->pivot_models) )
{
// Its a pivot model
foreach ($this->pivot_models[$entity] as $related_model => $table) // Transaction, Template
{
$related_model_lowercase = strtolower($related_model);
$this->entity_ids[$related_model] = DB::table($table)
->whereIn($entity_lowercase . '_id', $ids)
->lists($related_model_lowercase . '_id');
if( $this->getPivotIds($this->entity_ids[$related_model], $related_model) )
{
unset($this->entity_ids[$related_model]);
}
}
return true;
}
return false;
}
}
To check if given model is related to another one, which is what you want if I get you right, all you need is this tiny method making the most of Eloquent:
(Implement it in BaseModel, Entity or a scope, whatever suits you)
// usage
$task->isRelatedTo('transactions.users', $id);
// or
$template->isRelatedTo('tasks.transactions.users', Auth::user());
// or any kind of relation:
// imagine this: User m-m Transaction 1-m Item m-1 Group
$group->isRelatedTo('items.transaction.users', $id);
The magic happens here:
/**
* Check if it is related to any given model through dot nested relations
*
* #param string $relations
* #param int|\Illuminate\Database\Eloquent\Model $id
* #return boolean
*/
public function isRelatedTo($relations, $id)
{
$relations = explode('.', $relations);
if ($id instanceof Model)
{
$related = $id;
$id = $related->getKey();
}
else
{
$related = $this->getNestedRelated($relations);
}
// recursive closure
$callback = function ($q) use (&$callback, &$relations, $related, $id)
{
if (count($relations))
{
$q->whereHas(array_shift($relations), $callback);
}
else
{
$q->where($related->getQualifiedKeyName(), $id);
}
};
return (bool) $this->whereHas(array_shift($relations), $callback)->find($this->getKey());
}
protected function getNestedRelated(array $relations)
{
$models = [];
foreach ($relations as $key => $relation)
{
$parent = ($key) ? $models[$key-1] : $this;
$models[] = $parent->{$relation}()->getRelated();
}
return end($models);
}
Hey, but what's going on there?
isRelatedTo() works like this:
check if passed $id is a model or just an id, and prepares $related model and its $id for use in the callback. If you don't pass an object then Eloquent needs to instantiate all the related models on the $relations (relation1.relation2.relation3...) chain to get the one we are interested in - that's what happens in getNestedRelated(), pretty straightforward.
then we need to do something like this:
// assuming relations 'relation1.relation2.relation3'
$this->whereHas('relation1', function ($q) use ($id) {
$q->whereHas('relation2', function ($q) use ($id) {
$q->whereHas('relation3', function ($q) use ($id) {
$q->where('id', $id);
});
});
})->find($this->getKey());
// returns new instance of current model or null, thus cast to (bool)
since we don't know how deeply the relation is nested, we need to use recurrency. However we pass a Closure to the whereHas, so we need to use little trick in order to call itself inside its body (in fact we don't call it, but rather pass it as $callback to the whereHas method, since the latter expects a Closure as 2nd param) - this might be useful for those unfamiliar Anonymous recursive PHP functions:
// save it to the variable and pass it by reference
$callback = function () use (&$callback) {
if (...) // call the $callback again
else // finish;
}
we also pass to the closure $relations (as an array now) by reference in order to unshift its elements, and when we got them all (meaning we nested whereHas), we finally put the where clause instead of another whereHas, to search for our $related model.
finally let's return bool
There's really no easy nor canonical way, but here's a raw example of what I'd try to do.
class Entity extends Eloquent {
public function isRelatedTo($instance, $through)
{
$column = $instance->joiningTable($through) . '.' . $instance->getForeignKey();
$query = DB::table('');
this->buildJoin($query, $instance, $through);
return $query->where($column, '=', $instance->getKey())->exists();
}
public function relatesToMany($related, $through)
{
$that = $this;
$related = new $related;
return $related->whereIn($related->getKeyName(), function($query) use ($that, $related, $through) {
$that->buildJoin($query, $related, $through);
})->get();
}
protected function buildJoin($query, $related, $through)
{
$through = new $through;
$this_id = $this->getForeignKey();
$related_id = $related->getForeignKey();
$through_id = $through->getForeignKey();
$this_pivot = $this->joiningTable($through);
$related_pivot = $related->joiningTable($through);
$query->select($related_pivot . '.' . $related_id)->from($related_pivot)
->join($this_pivot, $related_pivot . '.' . $through_id, '=', $this_pivot . '.' . $through_id)
->where($this_pivot . '.' . $this_id, '=', $this->getKey());
}
}
Then, for your use case:
class User extends Entity {
public function isOwnerOf($task)
{
return $this->isRelatedTo($task, 'Transaction');
}
public function tasks()
{
return $this->relatesToMany('Task', 'Transaction');
}
}
Disclaimer: the code has not been tested.
Note that, in this very simplified example, relatesToMany directly returns a Collection. To have more advantages, it could instead return an instance of your own extension of Eloquent's Relation class - that takes longer to implement, clearly.
It shouldn't be difficult to add support for multiple intermediate entities; you could likely expect the $through argument to possibly be an array and then build the multi-join query accordingly.
If this is a Laravel project, then yes, you're trying far too hard.
If you're going to use Laravel, it's recommended that you use the features provided to you with Laravel, which are namely it's ORM, Eloquent, and it's bundled Schema tool. I'd recommend that you view Laravel's Getting Started page in their documentation, so that you can set your project up correctly to use Eloquent.
It would also be beneficial if you read up on the basics of how Eloquent handles relations in their models, as they do all of the work that you're trying to do.

Categories