How to chain DB relationships in Laravel (multiple has_many?) - php

I'm using Laravel, which is awesome, but I'm stuck on an issue with the database.
Let's say we have three tables, like so:
TABLE 1: pages
id | route | title
TABLE 2: elements
id | page_id | type
TABLE 3: content
id | element_id | data
I'd like to do a single selection for the page that will in turn select all of the elements with that page id, and for each of the elements it should select all of the content rows with the element id.
I want to have a static load_by_route($route) function in the Page model that, when called, will use the route to load and return the page info as well as the elements and content as described above. Ideally it would return a single object/array with all of this info.
Basically, I'm not sure how to chain the has_many() calls together so that I get the two-level relationship.

Look into eager loading. This should do what you want.
class Page extends Eloquent {
public function elements()
{
return $this->has_many( 'Element' );
}
}
class Element extends Eloquent {
public function contents()
{
return $this->has_many( 'Content' );
}
}
class Content extends Eloquent {}
$page = Page::with( array( 'elements', 'elements.contents' ) )->first();
https://laravel.com/docs/master/eloquent-relationships#eager-loading

Collin James answer gives you one object with all the data. I came here because I just wanted to iterate over all contents that belong to a page. Here is how you get such a collection:
$page = Page::with('elements.contents.element')->has('elements.contents');
$contents = [];
foreach ($page->elements as $element) {
$contents = $element->contents->merge($temp);
}
The with makes sure that you use eager loading and the has makes sure that we only iterate over elements with content.
From each content element you can get the element info from the belongsTo relationship that we also received with eager loading:
class Content extends Eloquent
{
public function element()
{
returh $this->belongsTo('\App\Page');
}
}

Related

How can I transform / cast a PHP parent object to a child object?

Reason
I got a legacy system with a table containing slugs.
When those records match, it represents some kind of page with a layout ID.
Because these pages can have different resource needs it depends on the layout ID which tables can be joined with.
I use Laravel's Eloquent models.
What I would like is to have a child model that holds the layout specific relations.
class Page extends Model {
// relation 1, 2, 3 that are always present
}
class ArticlePage extends Page {
// relation 4 and 5, that are only present on an ArticlePage
}
However in my controller, in order to know which layout I need, I already have to query:
url: /{slug}
$page = Slug::where('slug', $slug)->page;
if ($page->layout_id === 6) {
//transform $page (Page) to an ArticlePage
}
Because of this I get an instance of Page, but I would like to transform or cast it to an instance of ArticlePage to load it's additional relations. How can I achieve this?
You'll need to look into Polymorphic relations in Laravel to achieve this. A Polymorphic Relation would allow you to retrieve a different model based on the type of field it is. In your Slug model you would need something like this:
public function page()
{
return $this->morphTo('page', 'layout_id', 'id');
}
In one of your service providers, e.g. AppServiceProvider you would need to provide a Morph Map to tell Laravel to map certain IDs to certain model classes. For example:
Relation::morphMap([
1 => Page::class,
// ...
6 => ArticlePage::class,
]);
Now, whenever you use the page relation, Laravel will check the type and give you the correct model back.
Note: I'm not 100% sure on the parameters etc. and I haven't tested but you should be able to work it out from the docs.
If your layout_id is on the Page model, the only solution I see is to add a method to your Page model that is able to convert your existing page into an ArticlePage, or other page type, based on its layout_id property. You should be able to try something like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
const LAYOUT_ARTICLE = 6;
protected $layoutMappings = [
// Add your other mappings here
self::LAYOUT_ARTICLE => ArticlePage::class
];
public function toLayoutPage()
{
$class = $this->layoutMappings[$this->layout_id];
if (class_exists($class)) {
return (new $class())
->newInstance([], true)
->setRawAttributes($this->getAttributes());
}
throw new \Exception('Invalid layout.');
}
}
What this does is look for a mapping based on your layout_id property, and then it creates a new class of the correct type, filling its attributes with those from the page you're creating from. This should be all you need, if you take a look at Laravel's Illuminate\Database\Eloquent::newFromBuilder() method, which Laravel calls when it creates new model instances, you can see what's going on and how I've gotten the code above. You should be able to just use it like this:
$page = Slug::where('slug', $slug)
->first()
->page
->toLayoutPage();
That will give you an instance of ArticlePage
As far as I know there is no built in function for this.
But you could do something like this.
$page = Slug::where('slug', $slug)->page;
if ($page->layout_id === 6) {
$page = ArticlePage::fromPage($page);
}
And then on the ArticlePage create the static method
public static function fromPage(Page $page)
{
$articlePage = new self();
foreach($page->getAttributes() as $key => $attribute) {
$articlePage->{$key} = $attribute;
}
return $articlePage
}
Depending on your use-case might be smart to create a static method that does this automatically on the relation page() for Slug.

Laravel | Eloquent Populate Foreign Data

I have a database which is set out with a news and a news categories tables.
I have news which has a category 1 which links to a foreign key of the categories table where the category value equals the id in the categories table.
I'm trying to make a controller that gets the category name rather than the category ID when returning the results as a JSON response.
So far I have this inside of a model:
public function category()
{
return $this->belongsTo(NewsCategories::class);
}
And then I'm doing this inside of a controller:
public function index()
{
$news = new News();
$news = $news->category()->get();
return response()->json(['data' => $news], 200);
}
But the JSON response that gets returned is empty. I have googled some things but haven't really found anything useful regarding getting the foreign field which is title within the categories table.
This is the response that I get
{
data: [ ]
}
The first issue is that you're under the impression that your new News instance has linked categories:
$news = new News();
This will yield just an empty model instance; it has no database representation yet. Try fetching categories through a populated model instance:
$news = News::first();
// Or:
$news = News::find(1);
and retry the JSON response.
Second issue is that by calling $news->category()->get() you're actually querying the relation. If you only need to access the title, try $news->category->title which will load the associated category record and access the title field for you.
Regarding your comment, read on eager/lazy loading.

Laravel PHP, getting clicked link class

I have a html5 table which is dynamically made from database items and it contains links, e.g. delete icon, which is in link tags . I want to be able to click the delete icon and know, which item I want to delete. I have set class of the link the same as the relevant database items ID, but I cant read the class from the controller, after I click the link. Is there a method of doing that in PHP Laravel? Or maybe you could suggest a way better way to accomplish that? This seems a way off tactic for this.
If each row on the table represents a row on database, so your link could contain the id from database.
For example, a user table.
Row 1 => Link /users/delete/1
Row 2 => Link /users/delete/2
Row 3 => Link /users/delete/3
By doing it this way, you can know for sure which one is called.
On your routes file, if you are not using Route::resource(), you should have something like this:
Route::get('users/delete/{id}', 'UsersController#destroy');
And in your destroy method:
public function destroy($id)
{
// your logic here
}
Format your links as:
If for example you are listing all items using foreach:
#foreach( $items as $item )
{{$item->name}}
#endforeach
Inside routes.php
Route::get('item/delete/{id}', 'ItemsController#deleteItem');
inside ItemsController.php define the following function
public function deleteItem($id) {
$item = Item::get($id);
if( !$item ) App::abort(404);
$item->delete();
return Redirect::to('/');
}
and I am assuming you have your model in Item.php
class Item extends Eloquent {
protected $table = 'items';
}
and your items table has id and name columns

Relationship Design

I'm building an app where clients can create a document from a pre-defined template, edit some fields with their own text and save it. I've sketched out the relations as I think they would be, and it's mostly fine to convert into Laravel:
The only question I have is how I'd handle the FieldValue relationship. The idea is that the Template defines all the fields, then rather than re-create these on each Document, it should just look to its Template for them. That would mean the FieldValue needs to look up to its Document, to the Template of that and find the corresponding Field from there.
Is there a clean way to implement this, or is there a better way of designing the relationship to make it more practical to implement?
Going by your diagram, looks like a pivot table with pivot data...
Which would generally be modeled like this in Laravel:
class Document extends Model
{
public function template()
{
return $this->belongsTo('App\Template');
}
public function fields()
{
return $this->belongsToMany('App\Field')->withPivot('value');
}
}
class Template extends Model
{
public function organisation()
{
return $this->belongsTo('App\Organisation');
}
public function documents()
{
return $this->hasMany('App\Document');
}
public function fields()
{
return $this->hasManyThrough('App\Field', 'App\Section');
}
public function sections()
{
return $this->hasMany('App\Section');
}
}
class Section extends Model
{
public function fields()
{
return $this->hasMany('App\Document')->withPivot('value');
}
public function template()
{
return $this->belongsTo('App\Template');
}
}
class Field extends Model
{
public function documents()
{
return $this->belongsToMany('App\Document')->withPivot('value');
}
public function section()
{
return $this->belongsTo('App\Section');
}
}
class Organisation extends Model
{
public function documents()
{
return $this->hasManyThrough('App\Document', 'App\Template');
}
public function templates()
{
return $this->hasMany('App\Template');
}
}
With related tables (if sticking with laravel defaults):
fields
id - integer
section_id - integer
documents
id - integer
template_id - integer
templates
id - integer
organisation_id - integer
sections
id - integer
template_id - integer
organisations
id - integer
document_field
id - integer
document_id - integer
field_id - integer
value - string
Then you can access things many different ways. Here is one example:
$user = App\User::find(3);
$organisation = $user->organisation;
foreach ($organisation->documents as $document)
{
foreach ($document->fields as $field)
{
echo $field->pivot->value;
}
}
And inserting:
$field = App\Field::find(2);
$document = App\Document::find(4);
$value = 'My field value';
$document->fields()->save($field, ['value' => $value]);
Relevant docs:
Many-to-many relationships
Querying relationships
Inserting related models
Working with pivot tables
Hope this is what you meant:
$doc = Document::findOrFail(Input::get('docId'));
$sections = $doc->template->sections;
$fieldValues = $doc->field values;
Now you simply run on the field values and get the field and the section and start placing stuff.
For better performance I would eager load the fieldValue parameters with:
->with('field');
To give a answer to you're first question:
I suggest that you call it content. You can handle the content as you're doing now. The relationship as is is good enough. You've to do that separated from the other tables.
Second question: a better way
I've created a ERD myself based on you're ERD:
I've changed a few thinks.
Not so important but a document is from a user.
Templates can have sub templates. The consequence of this is that you can reuse sub templates. E.g. if you've a logo you can just place that in you're document every time.
Due to the new template table you don't need sections anymore. With the new approach you can define sub sections/templates.
Properties are all put into one table. With this approach you can define infinite properties for fields. This can be allot more flexible. These are referenced to a field and the content. The content_id however isn't needed. I've just placed it there so you can easily check which field it applies to.
The main point of you're question was the FieldValue/Content. The content is referenced to the document. From the document you can fill in the fields of the template.
I hope this is clear for you. The advantages I see are:
More flexible in properties
Content lookup is easy and referenced to a field
I haven't changed anything on the content table. Just leave it as is. It's good that way! Hope this helps you. If you use Schema designer you can retrieve you're models very easily!

Laravel get data from many to many relation

For a school project, I'm creating a website in the Laravel framework.
I can't work with the data from a many-to-many table in my controller.
Here are my models:
class Item extends Eloquent {
public function Tags()
{
return $this->belongsToMany('Tag', 'items_has_tags', 'items_id', 'tags_id');
}
}
class Tag extends Eloquent {
public function LearningMaterials()
{
return $this->belongsToMany('LearningMaterial', 'learning_materials_has_tags', 'tags_id', 'learning_materials_id');
}
}
I want to iterate all tags from my items in my controller:
//get all items
$all = Item::where('public', '1')->get();
foreach($allLm as $item){
$tags = $item->Tags();
var_dump('will iterate');
foreach($tags as $t){
var_dump('will not iterate');
}
}
What is wrong with my code? How can I handle the tags from my items?
FYI: I'm creating a search engine. If the user inserts an input value like "mana", all items with tag "management" should be shown.
Laravel's belongsToMany method queries related models and doesn't get them. That's because you might want to have some additional constraints in your query. What you need to do is:
$tags = $item->Tags()->get();
Or simply:
$tags = $item->Tags;
Calling the relationship function will return a relation object (in this case an instance of BelongsToMany). You can use this to run add further components to your query before running it.
For example:
$only4Tags = $item->Tags()->take(4)->get();
If you want the result of your relation, call get() or simpler, just use the magic property:
$tags = $item->Tags()->get();
$tags = $item->Tags;

Categories