How to insert with Eloquant a multi level entry? - php

Let's consider I have Quizzes that can contain Questions which each of them contains some Propositions. Therefore I have three models: Quiz, Question and Proposition. The propositions are linked to the questions through a pivot table that contains: id_question, id_proposition, is_correct.
In a Seeder, I would like to insert a dummy quiz which could be represented in YAML as follow:
title: A Quiz
questions:
- name: Animals
content: What's the difference between a duck?
propositions:
- text: The dog has two legs
is_correct: false
- text: One leg is both the same
is_correct: true
- text: Every duck has at least several teeth
is_correct: false
Traditionally in Laravel you need to split each step such as:
$quiz = new Quiz;
$quiz->name = "A Quiz";
$question = $quiz->questions()->create();
...
Is there a shorter way to hierarchically create a new entry that would look like this:
Quiz::create([
'name' => 'A Quiz'
'questions' => [
Question::Create([
'name' => 'Animals',
'content' => "What's the difference between a duck?"
'propositions' => [
Proposition::Create(['content' => 'The dog has two legs']),
Proposition::Create(['content' => 'One leg is both the same'])
Proposition::Create(['content' => 'Every duck has at least several teeth'])
]
])
]
]

just testing with static values :
$quiz = Quiz::create([
'name' => 'A quiz',
])->questions()->createMany([
['name' => 'animals', 'content' => 'animals content'],
['name' => 'fruits', 'content' => 'fruits content'],
])->map(function($value, $key){
$value->propositions()->createMany([
['content' => 'content 1'],
['content' => 'content 2'],
]);
});
i think you can set the values based on YAML index-values(something like json) with foreach

Related

Extract array entries to fit perfectly fit cases

I am building a student application where I need to find what files are the best match for the students based on the files that have number of the semesters they need to cover.
Each student is uploading a file where they select a number of semesters that are covered. Let's say file X has semester 1 and 2, file Y has semesters 1, 3, 4 and so on.
The only order here is the ascending order. The number of selected semesters can be random (1 or 1,2,3 or 3,4 or or 2,5,6 or 1,2,5,6,8 or any combination of this kind).
What i need to achieve is to get the least number of files covering the maximum amount of semesters taking into account the smaller semester to be covered.
In my example a special case is required semesters 5 where I could return files 1, 2, 3, 4 because all this contains semesters to be covered but this is not what I am looking for. Instead i need to get file 3 and 4 which covers perfectly the case.
I am providing files where each file is covering a number of semesters like this:
Student files in the following files:
file 1 covers semester: array(3)
file 2 covers semester: array(3,4)
file 3 covers semester: array(1,2)
file 4 covers semester: array(3,4,5,6)
$aUserFiles = [
0 => [
'file' => 'file 1',
'semesters' => [
3
]
],
1 => [
'file' => 'file 2',
'semesters' => [
3,4
]
],
2 => [
'file' => 'file 3',
'semesters' => [
1,2
]
],
3 => [
'file' => 'file 4',
'semesters' => [
3,4,5,6
]
]
];
Semesters that needs to be covered and expected results:
Option 1: number of semesters required: 5.
Expected result: file 3, file 4
$aResult = [
0 => [
'file' => 'file 3',
'semesters' => [
1,2
]
],
1 => [
'file' => 'file 4',
'semesters' => [
3,4,5,6
]
]
];
Option 2: number of semesters required: 2.
Expected result: file 3
$aResult = [
0 => [
'file' => 'file 3',
'semesters' => [
1,2
]
]
];
Option 3: number of semesters required: 4.
Expected result: file 3, file 2
Option 4: number of semesters required: 3.
Expected result: file 3, file 1
You can do something like this:
$requiredSemesters = 3; // Your input
$indicators = [];
$result = [];
foreach ($aUserFiles as $key => $file) {
$first = $file['semesters'][0];
$last = $file['semesters'][count($file['semesters']) - 1];
$filesInRangeCount = count(array_intersect($file['semesters'], range(1, $requiredSemesters)));
if (!isset($indicators[$first]) || $indicators[$first]['range'] < $filesInRangeCount) {
$indicators[$first] = ["key" => $key, "max" => $last, "range" => $filesInRangeCount];
}
}
ksort($indicators);
$result = [];
$max = 0;
foreach ($indicators as $indicator) {
if ($max >= $requiredSemesters) {
break;
}
$result[] = $aUserFiles[$indicator['key']];
$max = $indicator["max"];
}
print_r($result);
Demo: https://3v4l.org/ZfDLo
Explanation:
I make a new array and insert the values grouped by minimum value (first number since your arrays are ordered). In that process I make sure to leave only the file with highest number of overlap with your input (1-input) and (first number - last number). Now I sort the grouped array, and iterate. All I need to do now is to add the files until I reach the maximum number which is the given input.

Scoring results, then sorting it to show much relevant result

I'm working on trying to figure out how to show a search result from closest match to least closest.
Let's assume this is the multidimensional array of results. You will notice that there are arrays with the same "id", but have different "categories". I'm pretending this is a one-to-many relationship. So I'm assuming, for 1 "id", a user might have tagged it to 3 different relevant categories.
$results[] = array(
'id' => 1 ,
'text' => 'this is my first post',
'category' => 'blue'
);
$results[] = array(
'id' => 1 ,
'text' => 'this is my first post',
'category' => 'green'
);
$results[] = array(
'id' => 1 ,
'text' => 'this is my first post',
'category' => 'purple'
);
$results[] = array(
'id' => 2 ,
'text' => 'this is my second post',
'category' => 'blue'
);
$results[] = array(
'id' => 2 ,
'text' => 'this is my second post',
'category' => 'green'
);
Now, let's assume there are criteria that the user selected. I'll show it in array form:
$criterias = array('blue', 'green', 'purple');
Using this example, that means the $results "id" of 1 should show up first, and I want to show it's "text". This is because it scored 3 out of 3 (based on matching the criteria that was set in $criterias). Then following this logic the $results "id" of 2 should show up second because it only scored a 2 out of 3.
The final form what what I'm looking to do is be able to echo out the "text" value from highest score to lowest.
My level of programming in PHP is intermediate, so if you could please demonstrate a less complex solution that an intermediate could understand that would be great.
What I tried and didn't get to work was trying to first try to score it and put it into another multidimensional array and sort it, then echo it, but I couldn't get it to work.

Best way to convert keys in PHP

Im retrieving data from a mysql database like following Array:
$data = [
0 => [
'id' => 1,
'Benutzer' => 'foo',
'Passwort' => '123456',
'Adresse' => [
'Strasse' => 'bla', 'Ort' => 'blubb'
],
'Kommentare' => [
0 => ['Titel' => 'bar', 'Text' => 'This is great dude!'],
1 => ['Titel' => 'baz', 'Text' => 'Wow, awesome!']
]
],
]
Data like this shall be stored in a mongo database and therefore i want to replace the keynames with translated strings that come from a config- or languagefile ('Benutzer' -> 'username').
Do i really have to iterate over the array and replace the keys or is the a better way to achieve that?
If you don't want to iterate over the array then you can change the column name in the query itself using select() function.
Considering your model name is Client then your query will be:
Client::select('Benutzer as username', '...') // you can use `trnas()` function here also
->get()

How to concatenate values of a laravel collection

Here is a small challenge for Laravel fanboys :-)
I want to build a simple list of request segments along to their url.
I start with:
// http://domain/aaa/bbb/ccc/ddd
$breadcrumbs = collect(explode('/', $request->path()))
But I don't know how to map it to a collection looking like:
$breadcrumbs = collect([
['title' => 'aaa', 'link' => 'http://domain/aaa'],
['title' => 'bbb', 'link' => 'http://domain/aaa/bbb'],
['title' => 'ccc', 'link' => 'http://domain/aaa/bbb/ccc'],
['title' => 'ddd', 'link' => 'http://domain/aaa/bbb/ccc/ddd'],
])
I could easily do it with a for loop but I am looking for a really elegant way to do it. I tried with map() or each() without success.
As Adam Wathan says: "Never write another loop again." ;-)
There are quite a few ways you can go about doing this, but since you will inevitably require knowledge of past items, I would suggest using reduce(). Here's a basic example that will show you how to build up the strings. You could easily add links, make the carry into an array, etc.
collect(['aaa', 'bbb', 'ccc', 'ddd'])
->reduce(function ($carry, $item) {
return $carry->push($carry->last() . '/' . $item);
}, collect([]));
Results in
Illuminate\Support\Collection {#928
#items: array:4 [
0 => "/aaa"
1 => "/aaa/bbb"
2 => "/aaa/bbb/ccc"
3 => "/aaa/bbb/ccc/ddd"
]
}
Not claiming it's by any means optimised, but it does work. :)
This is old, but a bit different approach - works in Laravel 5.1 and up.
//your collection
$breadcrumbs = collect([
['title' => 'aaa', 'link' => 'http://domain/aaa'],
['title' => 'bbb', 'link' => 'http://domain/aaa/bbb'],
['title' => 'ccc', 'link' => 'http://domain/aaa/bbb/ccc'],
['title' => 'ddd', 'link' => 'http://domain/aaa/bbb/ccc/ddd'],
])
//one-liner to get you what you want
$result = explode(',', $breadcrumbs->implode('link', ','));
//here is what you will get:
array:4 [▼
0 => "http://domain/aaa"
1 => "http://domain/aaa/bbb"
2 => "http://domain/aaa/bbb/ccc"
3 => "http://domain/aaa/bbb/ccc/ddd"
]

Sort query results by nested association in cakephp 3

The Problem:
I have a cakephp 3.x query object with two nested associations, Organizations.Positions.Skills, that is being set to a view variable, $Organizations. I'm trying to sort the query's resulting top level array by a column in the first nested association. That is, I want to sort $Organizations by a column in Positions, specifically Positions.Ended).
public function index()
{
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills'
])
);
}
Model Info
Organizations has many Positions
Positions has many Skills
Research I've Done
order option
According to the cookbook find() has an order option: find()->order(['field']); or find('all', ['order'=>'field DESC']);
However, this only applies to fields in the table find() is being called upon. In this case, Organizations. For example, this is how it's typically used.
//This works.
//Sorts Organizations by the name of the organization in DESC.
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills'
])
->order(['organization DESC'])
);
but, trying to use it for nested associations doesn't work:
$this->set('Organizations',
this->Organizations->find(...)
->contain(...)
->order(['Organizations.Positions.ended DESC'])
);
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Organizations.Positions.ended' in 'order clause'
and altering it to refer to the field that'll be nested doesn't work either:
//also doesn't work.
$this->set('Organizations',
$this->Organizations->find(...)
->contain(...)
->order([Positions.ended DESC])
);
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Positions.ended' in 'order clause'
In both cases, the sql error is created when cakephp executes the PDO statement generated by the query.
sort option
Similarly, according to the cookbook, eager loading / associations has the 'sort' option:
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills',
'Positions' => [
'sort' => ['Positions.ended'=>'DESC']
]
])
);
But, this only sorts the nested association.Specifically, it sorts the associations that are nested. It does not sort the entire resulting set by the nested association (ergo, a multidimensional sort).
For example:
The Organization, Organization C (org id 1), has two positions:
Position 5. Ended 2012
Position 4. Ended 2014
And the Organization, Organization B (org id 2), has two positions:
Position 3 Ended 2013
Position 2 Ended 2015
The above code and data results in the following array being generated when the query is evaluated:
Organizations => [
0 => [
'organization' => 'Organization A',
positions => [
0 => [
'position' => 'Position 1',
'ended' => '2016'
]
]
],
1 => [
'organization' => 'Organization C',
'positions' => [
0 => [
'position' => 'Position 4',
'ended' => '2014'
],
1 => [
'position' => 'Position 5',
'ended' => '2012'
]
]
],
2 => [
'organization' => 'Organization B',
'positions' => [
0 => [
'position' => 'Position 2',
'ended' => '2015'
],
1 => [
'position' => 'Position 3',
'ended' => '2013'
]
]
]
]
other research
Likewise the following stackoverflow questions came up in my research:
http://stackoverflow.com/questions/26859700/cakephp-order-not-working
http://stackoverflow.com/questions/17670986/order-by-doesnt-work-with-custom-model-find-while-paginating
http://stackoverflow.com/questions/18958410/cakephp-paginate-and-sort-2nd-level-association
http://stackoverflow.com/questions/34705257/cakephp-paginate-and-sort-hasmany-association
Furthermore, I do know that PHP has its own sorting functions like sort() and multisort(); but, those can only be called once the query has been evaluated (by foreach). Alternatively, there's calling $organization->toArray() then using multisort; but, this would have to be done in the view, would break the MVC convention of separations of concerns (data and queries are manipulated by the controller and model, not the view!), and would be quite inefficient as it'll be called while the page is loading.
How then, do I sort a cakephp query by its nested associations?
Or, put more simply, how do I order/sort the query to produce the following array upon evaluation:
Organizations => [
0 => [
'organization' => 'Organization A',
'positions' => [
0 => [
'position' => 'Position 1',
'ended' => '2016'
]
]
],
0 => [
'organization' => 'Organization B',
'positions' => [
0 => [
'position' => 'Position 2',
'ended' => '2015'
],
1 => [
'position' => 'Position 3',
'ended' => '2013'
]
]
],
1 => [
'organization => 'Organization C',
'positions' => [
0 => [
'position' => 'Position 4',
'ended' => '2014'
],
1 => [
'position' => 'Position 5',
'ended' => '2012'
]
]
]
]
Background & Context:
I'm building a [portfolio website][7] for myself with cakephp 3.2 to showcase my web dev skills and assist in my quest for a dev career. For my resume page, I'm organizing the massive amount of data with nested accordions to mimic the resume style recruiters would expect to see on an actual resume. As a result, my view does the following:
Looping through the top level view variable (Organizations)
Rendering the organization details
Looping through that organization's positions (still inside 1)
render the position details
loop through the position's relevant skills
render each skill w/ the appropriate link to filter by that skill.
List item
Only hasOne and belongsTo associations are being retrieved via a join on the main query. hasMany associations are being retrieved in a separate queries, hence the errors when you try to refer to a field of Positions.
What you want should be fairly easy to solve, on SQL level, as well as on PHP level.
SQL level
On SQL level you could join in Positions, and order by a computed max(Positions.ended) column, like:
$this->Organizations
->find('all')
->select(['max_ended' => $this->Organizations->query()->func()->max('Positions.ended')])
->select($this->Organizations)
->contain([
'Positions.Skills',
'Positions' => [
'sort' => ['Positions.ended' => 'DESC']
]
])
->leftJoinWith('Positions')
->order([
'max_ended' => 'DESC'
])
->group('Organizations.id');
And that's all, that should give you the results that you want. The query will look something like:
SELECT
MAX(Positions.ended) AS max_ended,
...
FROM
organizations Organizations
LEFT JOIN
positions Positions
ON Organizations.id = (
Positions.organization_id
)
GROUP BY
Organizations.id
ORDER BY
max_ended DESC
PHP level
On PHP level it's also pretty easy to solve to using collections (note that queries are collections - kind of), however it would only make sense if you'd intend to retrieve all rows, or must deal with a set of unordered rows for whatever reason... something like:
$Organizations->map(function ($organization) {
$organization['positions'] =
collection($organization['positions'])->sortBy('ended')->toList();
return $organization;
});
$sorted = $sorted->sortBy(function ($organization) {
return
isset($organization['positions'][0]['ended']) ?
$organization['positions'][0]['ended'] : 0;
});
This could also be implemented in a result formatter, so things happen on controller or model level if you insist.
$query->formatResults(function ($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
$results = $results->map(function ($row) {
$row['positions'] =
collection($row['positions'])->sortBy('ended')->toList();
return $row;
});
return $results->sortBy(function ($row) {
return isset($row['positions'][0]['ended']) ?
$row['positions'][0]['ended'] : 0;
});
});
See
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Queries Are Collection Objects
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
Cookbook > Collections > Sorting
This worked for me:
$this->Organizations->find('all')
->contain(['Positions' => ['sort' => ['Positions.ended' => 'DESC']]])
->contain('Postions.Skills');

Categories