How to concatenate values of a laravel collection - php

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"
]

Related

Laravel | PHP - Segmenting an array of results to top 10

I'm new to PHP so my solution might be very inefficient so asking here to figure out something more efficient/better.
Consider an array of objects:
[
['sku' => 'AAA', 'amount' => ###],
['sku' => 'BBB', 'amount' => ###],
['sku' => 'CCC', 'amount' => ###],
['sku' => 'DDD', 'amount' => ###],
['sku' => 'EEE', 'amount' => ###],
['sku' => 'FFF', 'amount' => ###],
['sku' => 'GGG', 'amount' => ###],
['sku' => 'HHH', 'amount' => ###],
['sku' => 'III', 'amount' => ###],
['sku' => 'JJJ', 'amount' => ###],
['sku' => 'KKK', 'amount' => ###],
['sku' => 'LLL', 'amount' => ###],
['sku' => 'MMM', 'amount' => ###],
]
We want to keep the first 9 as they are, but consolidate the remaining under the 'sku' => 'Other' and some the amount.
Here is the working version of the code:
$data = DB::table('analytics')
->whereBetween('order_date', [$start, $end])
->whereIn('source', $suppliers)->select(
[
'sku',
DB::raw('SUM(analytics.price' . ($costs ? ' + ' . $costs : '') . ') as amount'),
]
)
->orderBy('amount', 'DESC')
->groupBy('sku')
->get();
$dataArray = $data->toArray();
$topNine = array_slice($dataArray, 0, 9);
$other = array_slice($dataArray, 9);
if (count($other)) {
$otherSum = array_reduce($other, function ($carry, $item) {
return $carry += moneyStringToFloat($item->cogs);
}, 0);
$otherObj = new stdClass();
$otherObj->sku = 'Other';
$otherObj->cogs = floatToMoneyString($otherSum);
$topNine[] = $otherObj;
}
And the final result looks something like this:
[
['sku' => 'AAA', 'amount' => ###],
['sku' => 'BBB', 'amount' => ###],
['sku' => 'CCC', 'amount' => ###],
['sku' => 'DDD', 'amount' => ###],
['sku' => 'EEE', 'amount' => ###],
['sku' => 'FFF', 'amount' => ###],
['sku' => 'GGG', 'amount' => ###],
['sku' => 'HHH', 'amount' => ###],
['sku' => 'III', 'amount' => ###],
['sku' => 'Other', 'amount' => ###],
]
Is there a better way to do this. Is there a way to do it directly in QueryBuilder?
Thank you,
Laravel is about working with the Laravel Collection and all the methods it provides.
Firstly, you don't have to convert it to an array, work with the data as is. Collections has a slice method, and instead of reducing it has a sum method which does the same as you are doing. So instead of juggling between arrays, PHP functions etc. keep it simple, shorter and Laravelistic. Creating a default object in PHP, can be done multiple ways, I like to create arrays and cast them to objects, all methods are OK, but I feel this is cleanest and shortest.
$analytics = DB::table('analytics')->...;
$topNine = $analytics->slice(0, 9);
$otherSum = $analytics->slice(9)->sum(function ($analytic) {
return moneyStringToFloat($item->cogs);
});
$topNine->push((object)[
'sku' => 'Other',
'cogs' => floatToMoneyString($otherSum),
]);
return $topNine;
Your code was fine, I cleaned it up and used a more Laravel approach, hope you get inspiration from it.
As a bonus, you can use analytics as a model, create an Eloquent accessor. This can make your sum into this nice syntactic sugar using Higher Order Functions on the Collection methods.
class Analytic extends Model
{
protected function cogsPrice(): Attribute
{
return Attribute::make(
get: fn ($value) => moneyStringToFloat($this->cogs),
);
}
}
$topNine = $analytics->slice(0, 9);
$topNine->push((object)[
'sku' => 'Other',
'cogs' => floatToMoneyString($analytics->slice(9)->sum->cogsPrice),
]);
return $topNine;
Depending on the size of the data you're returning and what you intend to do with that data (i.e. do you need it all later?) you could return all the data and then manipulate it in memory.
$all = Analytics::orderBy('amount', 'DESC');
$merged = collect($all->take(9)->get(['sku', 'amount'])->toArray())
->merge(collect(array(['sku' => 'other', 'amount' => $all->skip(9)->sum('amount')])));
Alternatively, if you're only interested in the first 9 individual records and everything from the 10th record onward is of no interest and you don't require them later for any logic, you could get the first 9 and then everything else:
$top9 = Analytics::orderBy('amount', 'DESC')->take(9);
$other = collect(array(['sku' => 'other', 'amount' => Analytics::whereNotIn('sku', $top9->pluck('sku'))->sum('amount')]));
$merged = collect($top9->get(['sku', 'amount'])->toArray())
->merge($other);
The above option means not loading a potentialy large data set into memory and performing the limiting and summing operation on the database, but does require some additional calls to the database. So there is a trade off to be taken into consideration with these.
You can use take() or limit() here like this:
$data = DB::table('analytics')
->whereBetween('order_date', [$start, $end])
->whereIn('source', $suppliers)->select()
->orderBy('amount', 'DESC')
->groupBy('sku')
->get()
->take(9);
OR
$data = DB::table('analytics')
->whereBetween('order_date', [$start, $end])
->whereIn('source', $suppliers)->select()
->orderBy('amount', 'DESC')
->groupBy('sku')
->limit(9)
->get();
The Difference
Although they both do pretty much the same thing there's some difference worth knowing in them.
limit()
limit() only works on eloquent ORM or query builder objects. which means that the number n specified in the limit(n) once the query finds the number of record equal to this it will simple stop executing making query run faster.
Syntax:
limit(9)->get() // correct
get()->limit(9) // incorrect
take()
take() take will simply let the query run until the records are fetched and then it simply extracts the number of records specified, making it slower as compared to limit() but it has it's own uses such as having the count of all records but taking only few records as in your case.
Syntax:
take(9)->get() // correct
get()->take(9) // correct
Your Case
Since you want all the records here's what you can do is, once the data is fetched in $data, you can simply:
$topNine = $data->take(9)->toArray()
$other = $data->skip(9)->take(PHP_INT_MAX)->toArray() // Some very large number to take all the remaining records PHP_INT_MAX in that case
Here skip() basically skips any number of elements you want to exclude in $other. you can skip(count($topNine)).
Hope this makes thing easy to understand for you.

How can I manage some irregular array to regular array?

I write code with some array that have different structure, but I must extract the data to do something else. How can I manager these array?
The array's structure are as follow:
$a = [
'pos1' => 'somedata',
'pos2' => ['data2', 'data3'],
'pos3' => '';
];
$b = [
[
'pos1' => ['data1', 'data2', ['nest1', 'nest2']],
'pos2' => ['data1', 'data2', 'data3'],
],
['data1', 'data2'],
'data4',
];
The array's Index can be a key or a position, and the value of the corresponding index may be a array with the same structure. More tough problem is that the subarray can be nesting, and the time of the nesting has different length.
Fortunately, every array has it's owe fixed structure.
I want to convert the these array to the format as follow. When the index is a value, change it to the keyword; and if the index is a keyword, nothing changed.
$a = [
'pos1' => 'somedata',
'pos2' => [
'pos2_1' => 'data2',
'pos2_2' => 'data3'
],
'pos3' => '';
];
$b = [
'pos1' => [
'pos1_1' => [
'pos1_1_1' => 'data1',
'pos1_1_2' => 'data2',
'pos1_1_3' => [
'pos1_1_3_1' => 'nest1',
'pos1_1_3_2' => 'nest2',
],
],
'pos1_2' => [
'pos1_2_1' => 'data1',
'pos1_2_2' => 'data2',
'pos1_2_3' => 'data3',
],
],
'pos2' => ['data1', 'data2'],
'pos3' => 'data4',
];
My first solution is for every array, write the function to convert the format(the keyword will specify in function). But it is a huge task and diffcult to manage.
The second solution is write a common function, with two argument: the source array and the configuration that specify the keyword to correspondent value index. For example:
$a = [0, ['pos10' => 1]];
$conf = [
// It means that when the value index is 0, it will change it into 'pos1'
'pos1' => 0,
'pos2' => 1,
];
The common funciton will generate the result of:
$result = [
'pos1' => 0,
'pos2' => ['pos10' => 1],
]
But this solution will lead to a problem: the config is diffcult to understand and design, and other people will spend a lot of time to understand the format after conversion.
Is there are some better solution to manage these array that other people can easy to use these array?
Thanks.

Eloquent recursive relation

I have an issue where I'm trying to get all descendants of an object and keep only those with a specific property.
I have these relations:
public function getChildren()
{
return $this->hasMany(self::class, 'parent_id', 'id');
}
public function allChildren()
{
return $this->getChildren()->with('allChildren');
}
And I get this type of array for example:
$array = [
0 => ['name' => 'aaa', 'type' => 0, 'parent' => null, 'children' => [
1 => ['name' => 'bbb', 'type' => 1, 'parent' => null, 'children' => []],
2 => ['name' => 'ccc', 'type' => 0, 'parent' => null, 'children' => [
3 => ['name' => 'ddd', 'type' => 1, 'parent' => 2, 'children' => []]
]]
]],
4 => ['name' => 'eee', 'type' => 0, 'parent' => null, 'children' => []]
];
For this example, I would like to remove all objects that are of type 1 and get a clean array without those only.
I don't really understand why it is possible to get all descendats of an object but not be able to pass conditions.
Thanks in advance.
A collection only solution would be something like this (place the custom macro in a Service Provider of your application):
Collection::macro('whereDeep', function ($column, $operator, $value, $nested) {
return $this->where($column, $operator, $value)->map(function ($x) use ($column, $operator, $value, $nested) {
return $x->put($nested, $x->get($nested)->whereDeep($column, $operator, $value, $nested));
});
});
Then where needed call:
$yourArray->whereDeep('type', '!=', 1, 'children');
On your example, the macro works like this:
Filter all the elements where: type != 1
(the outer array will beuntouched as both items has type => 0)
For each element of the current array:
Retrive the children property and apply the same filtering to this subarray starting with the first point of this instructions.
Replace the children property with the new children property just filtered.
Anyways, you should try to deep dive into why the relation filtering doesn't work. That solution would be more efficient if optimized correctly.
I found a great solution where there is no need of all this recursion or any of these relationship calls so I share it:
Using: "gazsp/baum"
// get your object with roots method
$contents = Content::roots()->get();
// and simply run through the object and get whatever you need
// thanks to getDescendantsAndSelf method
$myArray = [];
foreach($contents as $content) {
$myArray[] = $content->getDescendantsAndSelf()->where('type', '!=', 1)->toHierarchy();
}
return $myArray;
This works for me the same way as the other method above.

php - Alternative to 'if' statement inside array

I've been reading around and cannot find a solution that works for the requirement I have. I need to dynamically add values to the 'add' part of this array depending on conditions. I know that you cannot put any if statements inside the array itself.
The correct syntax (from the documentation) is:
$subResult = $gateway->subscription()->create([
'paymentMethodToken' => 'the_token',
'planId' => 'thePlanId',
'addOns' => [
'add' => [
[
'inheritedFromId' => 'addon1',
'amount' => '10'
],
[
'inheritedFromId' => 'addon2',
'amount' => '10'
]
]
]
]);
From what I had read on a similar question on SO, I tried the following (where $addon1 and $addon2 would be the conditions set earlier in the code)
$addon1 = true;
$addon2 = true;
$subResult = $gateway->subscription()->create([
'paymentMethodToken' => 'the_token',
'planId' => 'thePlanId',
'addOns' => [
'add' => [
($addon1 ? array(
[
'inheritedFromId' => 'productAddon1Id',
'amount' => '10'
]) : false),
($addon2 ? array(
[
'inheritedFromId' => 'productAddon2Id',
'amount' => '10'
]) : false)
]
]
]);
But I get back a Warning: XMLWriter::startElement(): Invalid Element Name so I suspect that it does not like the structure and the code fails with a fatal error (interestingly, if I only set the first $addon to true it still comes up with the warning, but does actually work. With two it fails).
Is there another way to do this or did I get the syntax wrong?
I cannot hardcode all the possibilities due to the amount of possible product combinations.
Would appreciate and help. Thank you.
Don't try to do everything at once.
$add = [];
if( $addon1)
$add[] = ['inheritedFromId'=>.......];
if( $addon2)
.....
$subResult = $gateway->subscription()->create([
'paymentMethodToken' => 'the_token',
'planId' => 'thePlanId',
'addOns' => [
'add' => $add
]
]);
you can put if statements in array declarations, it's called ternary operations:
$myArray['key'] = ($foo == 'bar' ? 1 : 2);
this is the basis of how to use.
You're using the array() syntax together with the short array syntax ([]). See the manual. This means that e.g. your first element in add would be an array within an array. Perhaps that's why the XML error is occurring? Better would be:
'add' => [
($addon1 ?
[
'inheritedFromId' => 'productAddon1Id',
'amount' => '10'
] : null),
($addon2 ?
[
'inheritedFromId' => 'productAddon2Id',
'amount' => '10'
] : null)
]
]
Your syntax is fine. you are creating array with this structure
array (size=3)
'paymentMethodToken' => string 'the_token'
(length=9)
'planId' => string 'thePlanId'
(length=9)
'addOns' =>
array (size=1)
'add' =>
array (size=2)
0 =>
array (size=1)
...
1 =>
array (size=1)
...
My quess is that $gateway->subscription()->create() is does not like this structure of your array. Maybe the 'false' as value or the numeric keys. Check what does it expect and try again with new structure of the array.

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()

Categories