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');
}
Related
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.
I have two table
one is products another is offers
products
---------
id | offer_id
offers
---------
id | product_id
Now I want to get all offers against a product
In my product model I wrote
public function getOfferDetails()
{
return $this->belongsTo('App\Offer');
}
but it return null.
You want to get all offers that belong to a product, right? Have you tried this:
public function getOfferDetails()
{
return $this->hasMany('App\Offer');
}
In your Product model?
Need to define how your relationships are. Based on what you have
A Product can have 0 - many offers and an Offer belongs to 1 Product.
You will need some foreign key to match the models. OTB Laravel will attempt to use the method name appended with _id as the foreign key. Since your method name is different, you need to pass the foreign key as the second parameter in your relationship method.
Products Model should have
public function getOfferDetails()
{
return $this->hasMany('App\Offer', 'product_id');
}
Offer Model should have
public function getProduct()
{
return $this->belongsTo('App\Product', 'product_id');
}
Laravel Documentation that might help out as well.
This is what you're looking for I think:
public function getOfferDetails()
{
return $this->hasMany('App\Offer', 'id', 'offer_id');
// as ceejayoz mentioned in comments, Laravel should be able to detect this themselves. The below would work the exact same as above
// return $this->hasMany('App\Offer', 'id', 'offer_id')
}
$product->getOfferDetails()->get();
I am creating a basic forum just so I have something meaningful to do while learning Laravel.
So just like every forum is organized, on the main page i would like to have a list of categories and their subcategories, a total count of posts in every subcategory while also a link to latest post
So relationship is nested hasMany:
Category -> has Subcategories -> has Threads -> has Posts.
In controller method I have
$cat = Category::with('subcategory')->get();
return View::make('forum.index', compact('cat'));
and this works for basic list of categories and subcategories but I can't figure out the rest.
This sure doesnt work
Category::with('subcategory')->with('threads')->with('posts')->get();
since relation between them is not set. Looking at Laravel docs, there is hasManyThrough relation. Is that a solution?
class Category extends Eloquent {
public function subcategory() {
return $this->hasMany('Subcategory');
}
public function posts() { // not sure about this cause it doesnt work
return $this->hasManyThrough('Post', 'Thread');
}
}
And on top of that how do I get posts->count() for every subcategory? Is it possible to have it split? Chaining could get complicated..
EDIT
Table columns are
Categories
id | title
Subcategory
id | title | category_id
Threads
id | title | subcategory_id | user_id
Posts
id | title | body | thread_id | user_id
EDIT 2
What would be the code for grabing only latest post? This doesnt work
$data = Category::with('subcategories.threads')->with(array('posts' => function($query)
{
$query->take(1);
}))->get();
You have setup only one relation that works and that is:
class Category extends Eloquent {
public function subcategory() {
return $this->hasMany('Subcategory');
}
}
Declare other relationships in other models:
class Subcategory extends Eloquent {
public function threads() {
return $this->hasMany('Thread');
}
}
class Thread extends Eloquent {
public function posts() {
return $this->hasMany('Post');
}
}
Once you have declared relationships then you may use:
$categories = Category::with('subcategory.threads.posts')->get();
Since the one Category has many subcategories so use the plural name for subcategories instead of subcategory in your Category model, so for example, you may use:
class Category extends Eloquent {
public function subcategories() {
return $this->hasMany('Subcategory');
}
}
Then also:
$categories = Category::with('subcategories.threads.posts')->get();
All relationships will be retrieved as nested object collections. For example:
$categories->subcategories; // Will be a collection of Subcategory models
$categories->subcategories->threads // Will be a collection of Thread models
$categories->subcategories->threads->posts // Will be a collection of Post models
You may declare a hasManyThrough relationship between Subcategory and Post using something like this:
class Subcategory extends Eloquent {
public function threads() {
return $this->hasMany('Thread');
}
public function posts() {
return $this->hasManyThrough('Post', 'Thread');
}
}
Also, you may build a relationship between Category and Thread through Subcategory.
I have
QuestionCategory and Questions['category_id'] and ItemsOfQuestion['question_id']
This Code has Work for my I hope that is useful for you
$categories=ExamCategory::with(['question' => function ($query) use($exam){$query->where('exam_id','=',$exam->id)->with('Items');}])->get();
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.
For example, I have a Product, and I have a BaseProduct.
In the model for the Product, I've specified the following:
//In class Product
public function BaseProduct()
{
return $this->belongsTo("BaseProduct", "BaseProductId");
}
In the BaseProduct, I've specified the following relationship:
//In class BaseProduct
public function Products()
{
return $this->hasMany("Product", "ProductId");
}
If I were to select a product, like so:
$Product::first()
I could get the BaseProduct by doing the following:
$Product::first()->BaseProduct()->get();
Instead of getting the array of the result from that, how would I get the Model of the BaseProduct, so I can get all of the children of BaseProduct, meaning all Products that have a foreign key relating to this BaseProduct.
I've tried BaseProduct()->all(); instead, but it isn't a valid method.
Edit:
I've created the following chain of function calls - but it's awful.
return BaseProduct::find(Product::first()->BaseProduct()->getResults()['BaseProductId'])->Products()->getResults();
Final edit:
I had made a mistake in my BaseProduct model. In the Products() function, I had specified return $this->hasMany("Product", "ProductId"); where ProductId should have been BaseProductId.
After I fixed that, I could successfully use:
Product::first()->BaseProduct->products;
As Sheikh Heera had explained.
To get the children of the BaseProduct you may try this:
$bp = BaseProduct::with('Products')->get();
Now, you have a collection of BaseProduct so, you may use something like this:
$bp->first()->products
Or get the second item from collection
$bp->get(1)->products
Also, you may run a loop like this (most probably in the view after pass it):
// From the controller
$bp = BaseProduct::with('Products')->get();
return View::make('view_name')->with('baseProduct', $bp);
In the View
#foreach($baseProduct->products as $product)
{{ $product->field_name }}
#endforeach
Update: Yes, you may try this
$product = Product::first();
$baseProduct = $product->BaseProduct;
// Dump all children/products of this BaseProduct
dd($baseProduct->products->toArray());
You may chain like:
Product::first()->BaseProduct->products;
Update: Your table structure should look something like:
Table:baseproduct:
id(pk) | some_field | another_field
Table:products:
id(pk) | baseproduct_id(fk) | another_field
According to this table structure, relationship should be
// BaseProduct
public function Products()
{
return $this->hasMany("Product");
}
// Product
public function Products()
{
// second parameter/baseproduct_id is optional unless
// you have used something else than baseproduct_id
return $this->belongsTo("BaseProduct", "baseproduct_id");
}
$product = Product::find('id');
$baseProduct = $product->baseProduct()->getModel();
$baseProduct->products()->getModels();