How to insert relations based on a nested array in Laravel? - php

I have 3 tables tests, questions, options.
As you can imagine
a test has many questions
a question belongs to a test
a question has many options
an option belongs to a question
These relations are set up in the models already.
I got the data from the front end in this form:
array:3 [
"name" => "First Test"
"preparation" => "First Test prep"
"questions" => array:2 [
0 => array:2 [
"title" => "Some question"
"options" => array:4 [
0 => "a"
1 => "b"
2 => "c"
3 => "d"
]
]
1 => array:2 [
"title" => "Another question"
"options" => array:4 [
0 => "e"
1 => "f"
2 => "g"
3 => "h"
]
]
]
]
This data perfectly represents these relationships. In fact if I were using a NoSql database I would simply store this in the database.
My question is "what is the best way to store all of this data at once while using eloquent in Laravel"?
Note: It is in the form of Laravel's collection.

class Test extends Model {
$table = 'tests';
function questions(){
return $this->hasMany(Question::class, 'test_id');
}
}
class Question extends Model {
$table = 'questions';
function answers(){
return $this->hasMany(Answer::class, 'question_id');
}
function test(){
return $this->belongsTo(Test::class, 'test_id');
}
}
class Answer extends Model {
$table = 'answers';
function question(){
return $this->belongsTo(Question::class, 'question_id');
}
}
of course i showed only relations ,tables and foreign keys.
you can add any additional data to your models.

You have to create a structure of model objects from given data.
JMS Serializer is a great library to do that, and it has laravel integration.

Related

Adding element to an array of arrays

I want to call Model::insert() on this array to insert multiple data at once
"person" => array:2 [▼
0 => array:2 [▼
"name" => "Person 1"
"place" => "Place 1"
]
1 => array:2 [▼
"name" => "Person 2"
"place" => "Place 2"
]
]
but the Model excepts a foreign key constraint department_id before it can be inserted into the DB. How do I add department_id to each array inside person array. Is there any other way apart from using for loops to iterate and placing it manually?
Result Array Should look like
"person" => array:2 [▼
0 => array:2 [▼
"department_id" => 1
"name" => "Person 1"
"place" => "Place 1"
]
1 => array:2 [▼
"department_id" => 1
"name" => "Person 2"
"place" => "Place 2"
]
]
Update Person model and "department_id" to the $fillable array:
protected $fillable = [
...
'department_id',
];
Would be something similar to this:
<?php
// app/Models/Person.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Person extends Model
{
use HasFactory;
protected $fillable = [
'name',
'place',
'department_id', // add department_id
];
...
}
I think there is no escaping loop here. You can use array_map here, but it also runs the loop internally.
Person::insert(array_map(function ($person) {
$person['department_id'] = 1;
return $person;
}, $persons));
The solution was similar to the comment by Tim Lewis
$department->people()->insert(...)
but insert didn't seem to assign id automatically
$department->people()->createMany(...)
worked
Thank you

Sql query where table is connected to it self and create a array using php

I have 1 table name page_live. Now I want to display it if isMenu = 1. Now it is connected to it self with filed name parent_Id.
As an example -
I have page_live named test_page and this row is a parent of a row named inside-1. And inside-1 is parent of inside-2.
Now, I have to create array which will look like -
[0]=>{
'name' => 'test_page'
[0]=> {
'name' => 'inside-1'
[0] => {
'name' => 'inside-2'
}
}
}
This is my table -
Model PageLive
<?php
namespace App\Http\Models;
use Illuminate\Database\Eloquent\Model;
class PageLive extends Model
{
protected $table = 'page_live';
protected $fillable = ['name', 'adminId', 'slugName', 'description',
'imageId', 'metaTitle', 'metaDesc', 'metaKeyword', 'pageTypeId', 'parent_id',
'isHome', 'pageDraftId', 'themeLayoutId'];
public function parent()
{
return $this->belongsTo(App\Http\Models\PageLive::class, 'parent_id');
}
public function children()
{
return $this->hasMany(App\Http\Models\PageLive::class, 'parent_id');
}
}
Please help me.
Thank you.
You Need to use recursive relations in your Model:
public function childrenPages()
{
return $this->hasMany(PageLive::class, 'parent_id', 'id');
}
public function allChildrenPages()
{
return $this->childrenPages()->with('allChildrenPages');
}
Then in Controller:
$page = PageLive::with('allChildrenPages')->first();
$page->allChildrenPages; // collection of recursively loaded children
// each of them having the same collection of children:
$page->allChildrenPages->first()->allChildrenPages; // .. and so on
I can't guarantee it will be efficient for your data, I tried to give you the idea and part of the code, you need to test it definitely.
You can use recursive function to achieve this.
public function abc( $ar, $pid = null ) {
$op = array();
foreach( $ar as $item ) {
if( $item['parent_id'] == $pid ) {
$op[$item['id']] = $item;
// using recursion
$children = $this->abc( $ar, $item['id'] );
if( $children ) {
$op[$item['id']]['children'] = $children;
}
}
}
return $op;
}
Use function something like this and call it wherever you want.
You will get array structure like -
array:1 [▼
1 => array:5 [▼
"id" => 1
"name" => "Test Page"
"slugName" => "test-page"
"parent_id" => null
"children" => array:1 [▼
3 => array:5 [▼
"id" => 3
"name" => "Inside 1"
"slugName" => "test-page2"
"parent_id" => "1"
"children" => array:1 [▼
4 => array:4 [▼
"id" => 4
"name" => "Inside 2"
"slugName" => "test-page3"
"parent_id" => "3"
]
]
]
]
]
]
Hope this will help you.

Laravel - replace null with empty array when no relation is found

Is it possible to replace null with an empty array when no relation is found?
E.g. The customer has contacts and contracts but one of the contract has no web.
$customers = Customer::with('contacts', 'contracts.web')
->orderBy('company')->orderBy('prename')->get();
The result would be as following...
2 => array:21 [
"id" => 1
"contacts" => array:2 [
0 => array:12 [
"id" => 1
"customer_id" => 1
]
1 => array:12 [
"id" => 2
"customer_id" => 1
]
]
"contracts" => array:2 [
0 => array:9 [
"id" => 1
"customer_id" => 1
"web" => array:7 [
"id" => 1
"contract_id" => 1
]
]
1 => array:9 [
"id" => 2
"customer_id" => 1
"web" => null // should be replaced with []
]
]
]
As I read in the docs (Constraining Eager Loads), it's only possible to manipulate the query with constraining eager loads.
UPDATE
Contract class
class Contract extends Model
{
public function web()
{
return $this->hasOne(Web::class);
}
}
For further readers here's an explanation how to solve this kind of problem.
Laravel returns an empty array if no records are found on a hasMany relation. If a hasOne relation is implemented, null will be returned.
So if you need an array also if no record is found on a hasOne relation, you need to do the following.
class Contract extends Model
{
public function web()
{
return $this->hasOne(Web::class)
->withDefault(function () {
return new Web();
});
}
}
As implemented like this its not possible to just return an empty array. Why this isn't possible, check out this issue on Laravel GitHub Issue Tracker.
There is existing code that depends on the result of any Eloquent relationship to either be null, a Model instance, or a Collection of Model instances. However, the current functionality of the withDefault() method opens up the potential for returning an object that is not one of those three expected values.
If you return a new \stdClass; or an empty array, an empty instance of web is returned. To get an empty array just instanciate a new Object of the relation class. In my case new Web();.
Your relationship method should be the one handeling this since it's the first place you can fix this
I checked this so it returns an array when the variable is null.
public class Contracts{
public function web(){
$collection = $this->hasMany('App\Web');
return $collection ? $collection : [];
}
}

How do I flatten laravel recursive relationship collection (tree collections)?

How do I flatten a collection with hierarchy self referenced models, tree collections into a single dimension collection. I have a self referencing model having parents and children.
I want the result to return a eloquent collection, not a simple collection or an array. array has been used as result results for easy demonstration
relationships are declared like this.
public function parent()
{
return $this->belongsTo(self::class, 'parent_id');
}
public function parentRecursive()
{
return $this->parent()->with('parentRecursive');
}
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
public function childrenRecursive()
{
return $this->children()->with('childrenRecursive');
}
so when i call the model->childrenRecursive it returns the collection as it should be. like this. i have changed it toArray() to make it easy to read.
array:1 [
0 => array:6 [
"id" => 5
"name" => "I am a child of 1"
"parent_id" => "1"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
"children_recursive" => array:1 [
0 => array:6 [
"id" => 6
"name" => "I am child of 5"
"parent_id" => "5"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
"children_recursive" => array:2 [
0 => array:6 [
"id" => 7
"name" => "I am child of 6"
"parent_id" => "6"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
"children_recursive" => []
],
1 => array:6 [
"id" => 8
"name" => "I am child of 6 too"
"parent_id" => "6"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
"children_recursive" => []
]
]
]
]
]
]
what I want to achieve is the collection to be single dimension. here is how the toArray() to that collection should look like.
array:4 [
0 => array:6 [
"id" => 5
"name" => "I am a child of 1"
"parent_id" => "1"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
],
1 => array:6 [
"id" => 6
"name" => "I am child of 5"
"parent_id" => "5"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
],
2 => array:6 [
"id" => 7
"name" => "I am child of 6"
"parent_id" => "6"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
],
3 => array:6 [
"id" => 8
"name" => "I am child of 6 too"
"parent_id" => "6"
"created_at" => "2016-12-26 13:53:50"
"updated_at" => "2016-12-26 13:53:50"
]
]
I have tried many collection methods like filter, flatMap, flatten and multiple array methods. but haven't found an appropriate solution.
It's a bit late, but I'm going to post what I wish I had been able to find before I ended up writing it myself.
Similar to the original post, I have a recursive parent/child relationship in my categories table (but this could apply to any table with a self-referencing parent_id column). You can set up your Model like this:
Category.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model {
// Relationships
public function parent()
{
return $this->belongsTo('App\Models\Category', 'parent_id');
}
public function children()
{
return $this->hasMany('App\Models\Category', 'parent_id');
}
public function nested_ancestors()
{
return $this->belongsTo('App\Models\Category', 'parent_id')->with('parent');
}
public function nested_descendants()
{
return $this->hasMany('App\Models\Category', 'parent_id')->with('children');
}
// Attributes
public function getFlatAncestorsAttribute()
{
return collect(flat_ancestors($this));
}
public function getFlatDescendantsAttribute()
{
return collect(flat_descendants($this));
}
}
Then somewhere in your application, you need to have a place to put some global helper functions. You could follow the instructions found here, and then just paste in the following helper functions:
Helpers.php
function flat_ancestors($model) {
$result = [];
if ($model->parent) {
$result[] = $model->parent;
$result = array_merge($result, flat_ancestors($model->parent));
}
return $result;
}
function flat_descendants($model) {
$result = [];
foreach ($model->children as $child) {
$result[] = $child;
if ($child->children) {
$result = array_merge($result, flat_descendants($child));
}
}
return $result;
}
The code above will then allow you to use $category->flat_ancestors, which will produce a flat collection of all the category's ancestors, no matter how many there are. Similarly, using $category->flat_descendants will yield a flat collection of all the child categories, and the child's children categories, and so on until all the posterity categories have been accounted for.
Some things to be careful of:
This type of approach could potentially lead to an infinite loop if
you have Category 1 referencing Category 2 as its parent, and
then Category 2 has Category 1 as its parent. Just be careful
that parent/child relationships are incest free :-)
This type of approach also isn't very efficient. It'll be fine for a bunch of
parent/child recursive relationships, but especially for the
flat_descendants functions, the number of database queries grows
exponentially for each generation level.
I didn't find any builtin method into theLaravel collection either. You may try something like this (Use it as a global function or as a dedicated class method, it's up to you. here is the idea):
function flatten($array) {
$result = [];
foreach ($array as $item) {
if (is_array($item)) {
$result[] = array_filter($item, function($array) {
return ! is_array($array);
});
$result = array_merge($result, flatten($item));
}
}
return array_filter($result);
}
Then use it like this:
// When available into global scope as a function
$flattenArray = flatten($arrayFromTheCollection);
This will will recursively flatten. It doesn't prevent duplicates though, so you'll need to filter them out if that's an issue.
In your AppServiceProvider::boot method
use Illuminate\Support\Collection;
//...
Collection::macro('flattenTree', function ($childrenField) {
$result = collect();
foreach ($this->items as $item) {
$result->push($item);
if ($item->$childrenField instanceof Collection) {
$result = $result->merge($item->$childrenField->flattenTree($childrenField));
}
}
return $result;
});
Then
$flattened = $myCollection->flattenTree('childrenRecursive');
// or in the case of the question
$flattened = $model->childrenRecursive->flattenTree('childrenRecursive');
this is my code, it might help ^_^
Collection::macro('flattenTree', function ($childrenField = 'children', $levelAttribute = 'level')
{
$toProcess = $this->items;
$processed = [];
while($item = array_shift($toProcess))
{
$item->$levelAttribute ++;
$processed[] = $item;
if (count($item->$childrenField) > 0) {
$children = array_reverse($item->$childrenField->items);
foreach ($children as $child) {
$child->$levelAttribute = $item->$levelAttribute;
array_unshift($toProcess,$child);
}
}
}
return Collection::make($processed);
});
you should put this code in the boot method of AppServiceProvider.php or any provider you wish, and then you can use it like this
Category::where('parent_category_id', null)->get()->flattenTree();
this will flat the tree and add a level attribute to each object to indicate the depth level of the object
good luck for everyone
For these who does run into a dead loop because of incest relationship, I used this solution to retrieve descendants' attributes through eager loaded relationship - worked like fully flattening the relationship but avoid running into dead loop by foreach.
Solution link

How can I select certain columns from my recursive child call

I'm pretty new to Eloquent and I'm having issues wrapping my head around something.
Basically I have a table which I'm recursively grabbing children from within the same table.
public function children() {
return $this->hasMany(static::class, 'parent_org_id');
}
public function childrenRec()
{
return $this->children()->with('childrenRec');
}
Where childrenRec is a recursive call to all children based on 'parent_org_id'
I'm calling it from the following in a static function, as of right now I only want the id and the name_en of the org
self::select('id','name_en')->where('parent_org_id','=',0)->with('childrenRec')->get()->toArray();
which is grabbing the top level org (my top level org has a parent_org_id of 0).
My issue is that in the recursively grabbed children it doesn't limit it to the id and the name_en
My question boils down to:
How can I select only certain columns from my recursive child calls, as well is this the 'proper' way of doing things?
My returned array looks like this.
array:1 [▼
0 => array:4 [▼
"id" => 1
"name_en" => "Org Unit"
"org_type" => null
"children_rec" => array:2 [▼
0 => array:27 [▼
"id" => 2
"name_en" => "My First Orgunit."
"code" => null
"abbreviation" => null
"address1" => "222 Street Street"
"address2" => null
"city_id" => 1
"province_id" => 14
"postalcode" => "C161L7"
"country_id" => 38
"contact_name" => null
"contact_title" => null
"email" => "test#test.com"
"fax" => "902-555-5555"
"phone1" => "5125125125125"
"phone2" => null
"org_type_id" => 1
"parent_org_id" => 1
"ref_id" => 79
"has_users" => 1
"created_at" => "2016-11-02 18:47:55"
"updated_at" => "2016-11-02 18:47:55"
"org_type" => array:4 [▶]
"children_rec" => array:1 [▶]
]
1 => array:27 [▶]
]
]
]
Thanks in advance.
To access the relation query in the with() method you use an array with the name of the relationship as the key and a closure with an instance of QueryBuilder injected.
One 'gotcha' that took me forever to track down a solution to when doing this is, your parent and children queries need to include the key that associates their relationship because the models are attached/associated with each other after both queries are run separately. Those models are associated with each other using the columns defined in the relation on the model. Without the columns used in the association of the models in your query, the related models won't be attached. In your situation it would be:
self::select('id','name_en')
->where('parent_org_id','=',0)
->with(['childrenRec' => function($query) {
return $query->select('id', 'name_en', 'parent_org_id');
}])
->get()
->toArray();
If you don't include the parent_org_id in the subquery the relationships won't get attached.
Docs
Try this:
public function childrenRec()
{
return $this->children()->with(['childrenRec' => function($query){
$query->select('id','name_en');
}]);
}

Categories