In my Laravel project I'm trying to flatten an array to ensure consistency, for some reason, the attached screenshot showing my data format returned from my project won't flatten with the flatten() method.
I get an error:
Error: Call to a member function flatten() on array
Which is quite generic, I've tried using ->toArray() before flattening but this doesn't give me any data, what am I doing wrong here?
The logic exists within a Laravel job, thus the console log
/**
* Group data
*
* #return void
*/
public function groupData(
$data,
$groupBy,
$groupByFormat,
$additionFromField = ''
) {
$results = $data->groupBy(function ($item, $key) use ($groupBy, $groupByFormat) {
$date = Carbon::parse($item->{$groupBy});
return $date->format($groupByFormat);
});
// grouping by some kind of total
if (!empty($additionFromField)) {
$results = $results->map(function ($item, $key) use ($additionFromField) {
$totals = 0;
foreach ($item as $key => $value) {
$totals += $value->{$additionFromField};
}
return [
'items' => count($item),
'total' => $totals ?? 0
];
});
$calcedData = [];
foreach ($results as $key => $result) {
array_push($calcedData, [
'period_to' => $key,
'items' => $result['items'],
'total' => $result['total']
]);
}
return $calcedData;
}
// standard grouping of data
$results = $results->map(function ($item, $key) {
return $item[0];
});
return $results;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$filters = json_decode($this->report->discovery_filters, true);
$data = [];
foreach ($filters as $findableKey => $findable) {
/*
** If there are datasets on the findable objec, then we assume
** that we can build up a chart or some data structure.
*/
if (isset($findable['datasets'])) {
$pushableDatasets = [];
foreach ($findable['datasets'] as $datasetKey => $dataset) {
// query data
if (isset($dataset['query'])) {
$additionFromField = $dataset['query']['additionFromField'] ?? '';
$res = DB::table($dataset['query']['table'])
->select($dataset['query']['columns'])
->where($dataset['query']['filterBy'])
->orderBy($dataset['query']['orderBy']['field'], $dataset['query']['orderBy']['direction'])
->get()
->chunk(100);
$res = $res->flatten();
if (isset($dataset['query']['useGrouping']) && $dataset['query']['useGrouping'] == 'yes') {
$results = $this->groupData(
$res,
$dataset['query']['groupBy'],
$dataset['query']['groupByFormat'],
$additionFromField
);
var_dump($results); // shown in the screenshot
$resultData = $results->flatten();
array_push($pushableDatasets, $this->getStructure($findable, $datasetKey, $resultData));
}
}
}
$findable['datasets'] = $pushableDatasets;
}
array_push($data, $findable);
}
}
Error: Call to a member function flatten() on array
The error message is quite accurate and descriptive. flatten is a member function of an object (in this case, the Laravel Collection object) and an array is not an object.
You need to convert the array to a collection first, then you can flatten it:
$flattened = collect($results)->flatten();
$new = collect($results)->flatten();
In your case you can also use array_flatten() I guess. Here is the documentation https://laravel.com/docs/5.1/helpers#method-array-flatten
Related
I have object of class $values like:
Array
(
[0] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => CONNECT_NETWORKS_ON_SIGN_UP
[value:App\ValueObject\Features:private] => 1
)
[1] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => SHOW_BILLING
[value:App\ValueObject\Features:private] => 1
)
[2] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => FEATURE_FLAGS
[value:App\ValueObject\Features:private] => 'activity'
)
)
All array keys are returning boolean type value expect one, which returns string value.
My result with the code:
$arrays = array_map(
function($value) { return [strtolower((string) $value->getFeature())]; },
iterator_to_array($values)
);
return array_merge(...$arrays);
returns list of feature names like:
"features": [
"connect_networks_on_sign_up",
"show_billing",
"feature_flags"
]
What I want to edit is that for the last one we write its value NOT feature name ($value->getValue())
I am assuming that using in_array() PHP function would be the best approach here but I can't find a way to use it within my current method.
Tried with foreach() loop but nothing happens, like it's something wrong:
$features = [];
foreach ($values as $value)
{
$setParam = $value->getFeature();
if ($value == 'FEATURE_FLAGS') {
$setParam = $value->getValue();
}
$features[] = strtolower((string) $setParam);
}
return $features;
Can someone help?
Thanks
You should probably operate on the feature code FEATURE_FLAGS, rather than assuming that the last feature in the array always contains the flags. Using your existing code, that could be as simple as:
$arrays = array_map(
function($value)
{
/*
* If the current Features object has the feature code FEATURE_FLAGS,
* return the value itself, otherwise return the feature code in lowercase
*/
return ($value->getFeature() == 'FEATURE_FLAGS') ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
If you want to define an array of feature codes that you need to treat this way, you can define it internally in the callback, but it is probably a better idea to define it externally. You can then pass it into the callback with use
/*
* Define an array of feature codes that we want to return
* values for
*/
$valueCaptureFeatures = ['FEATURE_FLAGS'];
$arrays = array_map(
function($value) use ($valueCaptureFeatures) // <-- Put our $valueCaptureFeatures in the scope of the callback
{
/*
* If the current Features object has a feature code in the $valueCaptureFeatures array,
* return the value itself, otherwise return the feature code in lowercase
*/
return (in_array($value->getFeature(), $valueCaptureFeatures)) ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
Working example:
// Mock the Features class
class Features
{
private $feature;
private $value;
public function __construct($feature, $value)
{
$this->feature = $feature;
$this->value = $value;
}
public function getFeature()
{
return $this->feature;
}
public function setFeature($feature): void
{
$this->feature = $feature;
}
public function getValue()
{
return $this->value;
}
public function setValue($value): void
{
$this->value = $value;
}
}
// Mock an iterator with test Feature instances
$values = new ArrayIterator( [
new Features('CONNECT_NETWORKS_ON_SIGN_UP', 1),
new Features('SHOW_BILLING', 1),
new Features('FEATURE_FLAGS', 'activity')
]);
/*
* Define an array of feature codes that we want to return
* values for
*/
$valueCaptureFeatures = ['FEATURE_FLAGS'];
$arrays = array_map(
function($value) use ($valueCaptureFeatures) // <-- Put our $valueCaptureFeatures in the scope of the callback
{
/*
* If the current Features object has a feature code in the $valueCaptureFeatures array,
* return the value itself, otherwise return the feature code in lowercase
*/
return (in_array($value->getFeature(), $valueCaptureFeatures)) ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
$output = array_merge(...$arrays);
$expectedResult = [
'connect_networks_on_sign_up',
'show_billing',
'activity'
];
assert($output == $expectedResult, 'Result should match expectations');
print_r($output);
In my Laravel 8 project I'm dispatching a Job which runs and collects a bunch of data from the database, the data could be any amount ranging from a few hundred rows of data to potentially thousands, so could be quite memory intensive.
Upon returning the results, they're processed and added to a database table, and I'm hoping to have some kind of progress indication as a percentage that can be reported back to the user whilst the chunking is in progress, I have two tables, a reports and a reports_data table.
I've switched by query over to Laravel's chunk method, and am splitting the data collection into smaller bits as this should improve performance, but for some reason, to use my data as a whole, as if it were a collection I'm pushing it into an empty array called $res, but I'm getting an error so my job failsError: Call to a member function groupBy() on array:
Error: Call to a member function groupBy() on array
I'm wondering what I'm missing...
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$filters = json_decode($this->report->discovery_filters);
$data = [];
// create
foreach ($filters as $findable) {
$resultData = [];
// query data
if (isset($findable->query)) {
$this->setDynamicChartOptions();
$res = [];
$chunkData = DB::table($findable->query->table)
->select($findable->query->columns)
->where($findable->query->filterBy)
->orderBy($findable->query->orderBy->field, $findable->query->orderBy->direction)
->chunk(100, function ($chunkedResults) use ($res) {
foreach ($chunkedResults as $chunk) {
// how to update some kind of progress?
array_push($res, $chunk);
var_dump(count($res));
}
});
// $res expected as a collection? Maybe I can use the `collect` method?
if (isset($findable->query->useGrouping) && $findable->query->useGrouping) {
$results = $res->groupBy(function ($item, $key) use ($findable) {
$date = Carbon::parse($item->{$findable->query->groupBy});
return $date->format($findable->query->groupByFormat);
});
$results = $results->map(function ($item, $key) {
return $item[0];
});
$resultData = $results->flatten();
}
}
$res = [
'componentID' => $findable->componentID ?? 0,
'type' => $findable->type ?? '',
'name' => $findable->name ?? '',
'labelsKey' => $findable->query->labelsKey ?? '',
'dataKey' => $findable->query->dataKey ?? '',
'data' => $resultData ?? [],
'structure' => $this->getStructure($findable, $resultData)
];
array_push($data, $res);
}
// create our report data entry
$this->createReportData($data);
}
UPDATE:
I've tried chunking and grouping, the job fails:
$res = [];
$chunkData = DB::table($findable->query->table)
->select($findable->query->columns)
->where($findable->query->filterBy)
->orderBy($findable->query->orderBy->field, $findable->query->orderBy->direction)
->chunk(100, function ($chunkedResults) use ($res) {
$res[] = $chunkedResults;
foreach($res as $chunk) {
$chunk->groupBy();
}
});
This also fails...
res = [];
$chunkData = DB::table($findable->query->table)
->select($findable->query->columns)
->where($findable->query->filterBy)
->orderBy($findable->query->orderBy->field, $findable->query->orderBy->direction)
->chunk(100, function ($chunkedResults) use ($res) {
$res[] = $chunkedResults;
});
foreach($res as $chunk) {
$chunk->groupBy();
}
And this too, still doesn't seem to work in that it doesn't give back any collection, which is what I need for the rest of my code to work:
$res = [];
$chunkData = DB::table($findable->query->table)
->select($findable->query->columns)
->where($findable->query->filterBy)
->orderBy($findable->query->orderBy->field, $findable->query->orderBy->direction)
->chunk(100, function ($chunkedResults) use ($res) {
foreach ($chunkedResults as $key => $chunk) {
array_push($res, $chunk);
}
});
$res = collect($res);
Because $res = []; is an array, not an instance of eloquent's Illuminate\Support\Collection. Therefore, you can not call $res->groupBy(), as you are trying within the first if-statement.
Remove the ->chunk() method and get your data-chunk by using slice instead for example within a loop that always takes a slice of the data.
Optionally, call collect($res) to turn the array back into a collection. However, when having a Collection already, there is no point in making it into an array first just to cast it back directly afterwords. So I would go with the slice approach.
You could also - withing your chunk callback - do the following:
->chunk(100, function ($chunkedResults) use ($res) {
$res[] = $chunkedResults;
});
And then:
foreach($res as $chunk) {
$chunk->groupBy();
}
I have a recursive $data structure that I need to modify. Each node considered an $item should get a property with the value of $value added. Things that I tried (and how they failed) are:
array_walk_recursive: Visits only leaf nodes.
Stack/queue: I failed to modify the original structure but only altered the copies on the stack/queue.
Loops: Without the stack/queue approach I would need to know the nesting level and write an awful lot of nested loops.
array_map: I failed to write a proper recursive callback given that the value of $value is not static but the result of previous code. So it must somehow get "into" the callback. Since use is only available to anonymous functions I did not manage to write a recursive one.
Loop and recursive function: This answer to a similar question failed for the same reason as the array_map approach.
My situation in code looks similar to this example:
<?php
$value = 'example';
$data = array(
'foo' => 'bar'
'items' => array(
array(
'foo' => 'bar',
'items' => array(
array('foo' => 'bar')
)
)
)
);
// do this recursively to every member of an 'items' property:
$item['baz'] = $value;
Can you think of a different approach or help me straighten out one of those that I failed at so far?
Update
Some code that I tried that did not work:
// Parse error: syntax error, unexpected 'use' (T_USE), expecting '{'
function do (&$item) use ($value) {
$item['baz'] = $value;
foreach ($item['items'] as $next) {
do($next);
}
}
// Undefined variable: value
function do (&$item) {
$item['baz'] = $value;
foreach ($item['items'] as $next) {
do($next);
}
}
foreach ($data['items'] as $item) {
do($item);
}
Works for now (I would prefer not having to pass the $value parameter, though):
function do (&$item, $value) {
$item['baz'] = $value;
foreach ($item['items'] as &$next) {
do($next, $value);
}
}
foreach ($data['items'] as &$item) {
do($item, $value);
}
Check this code for get each key and value:
<?php
error_reporting(0);
$value = 'example';
$data = array(
'foo' => 'bar',
'items' => array( array( 'foo' => 'bar','items' => array(array('foo' => 'bar') ) ) )
);
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data));
foreach ($iterator as $k => $v) {
echo $k.'=>'.$v;
echo '</br>';
}
?>
Formatting was necessary but did not suffice for the minimum edit length. So I added this otherwise useless text.
The following recursive function works for me. Note that it requires to pass parameters by reference also inside the foreach loop:
$value = 'example';
function do (&$item, $value) {
$item['baz'] = $value;
foreach ($item['items'] as &$next) {
do($next, $value);
}
}
foreach ($data['items'] as &$item) {
do($item, $value);
}
I use this method:
<?php
/**
* #param array $arr
* #param callable $callback
* #param array $options
*
*
* Example:
* (this will add the link property to every node in the array recursively)
*
*
* $linkFmt = "/mylink/{type}/{slug}";
* ArrayTool::updateNodeRecursive($ret, function (array &$row) use ($linkFmt) {
* $row['link'] = str_replace([
* "{type}",
* "{slug}",
* ], [
* $row['type'],
* $row['slug'],
* ], $linkFmt);
* });
*
*
*
*
*/
public static function updateNodeRecursive(array &$arr, callable $callback, array $options = [])
{
$childrenKey = $options['childrenKey'] ?? "children";
foreach ($arr as $k => $v) {
call_user_func_array($callback, [&$v]);
if (array_key_exists($childrenKey, $v) && $v[$childrenKey]) {
$children = $v[$childrenKey];
self::updateNodeRecursive($children, $callback, $options);
$v[$childrenKey] = $children;
}
$arr[$k] = $v;
}
}
I've tried to query using eloquent and fractal
$lists = Category::all();
$result = Fractal::collection($lists, new CategoryTransformer())->getArray();
and return it
return response()->json((['code' => "200", 'results' => $result]));
the json result is this:
{"code":"200","results":{"data":[{"id":"1","name":"Cafe","logo":null,"cover":""},{"id":"2","name":"SPA","logo":null,"cover":""},{"id":"3","name":"Hotel","logo":null,"cover":""}]}}
How to remove "data" after result?. So i can just get the array without "data".
I've tried:
$result = Fractal::collection($lists, new CategoryTransformer(), 'results')->getArray();
return (['code' => "200", $result]);
it return me :
{"code":"200","0":{"results":[{"id":"1","name":"Cafe","logo":"","cover":""},{"id":"2","name":"SPA","logo":"","cover":""},{"id":"3","name":"Hotel","logo":"","cover":""}]}}
There is leading '0' before results. how can i remove it?
Thanks
Try this:
return (['code' => "200", "results" => $result['results']);
I think the array method can't deal with a given array.
An other solution would be to add your results:
$result['code'] = 200;
return $result;
The data is just the key, I think it won't make any issues. If you still need to remove it, update getArray() function.
Put these Collection Macros in your AppServiceProvider::boot() method:
/**
* Remove the unnecessary nested 'data' keys
*
* #param string $case For consistency, define the type of keys that should be returned
*/
Collection::macro('fractal', function ($case = 'snake_case') {
//Handle this as a nested function to block access to the $depth flag.
//It's purpose is to indicate how deep the recursion is, and,
//more importantly, when it's handling the top-level instance
$recursion = function ($case = 'snake_case', array $items = [], $depth = 0) use (&$recursion) {
//If the array has only one element in it, and it's keyed off 'data', remove the wrapper.
//However, if it has a sibling element, such as 'meta', leave it alone
if (array_key_exists('data', $items) && count($items) == 1) {
$items = $items['data'];
}
$items = (new static($items))->mapWithKeys_v2(function ($item, $key) use (
$case,
$recursion,
$depth
) {
$key = $case ? $case($key) : $key;
//If the nested item is itself an array, recursively perform the same transformation
return is_array($item) ?
[$key => $recursion($case, $item, ++$depth)] : [$key => $item];
})->toArray();
//Maintain the top-level 'data' wrapper.
//This can easily be removed later in the controller if that's not needed either
$items = (!$depth && !array_key_exists('data', $items)) ?
['data' => $items] : $items;
return $items;
};
//Return the results in the form of an instance of Collection
return new static($recursion($case, $this->items));
});
/**
* Maintain non-sequential numeric keys when performing
* \Illuminate\Support\Collection::mapWithKeys() functionality
*
* Source: https://github.com/laravel/framework/issues/15409#issuecomment-247083776
*/
collect()->macro('mapWithKeys_v2', function ($callback) {
$result = [];
foreach ($this->items as $key => $value) {
$assoc = $callback($value, $key);
foreach ($assoc as $mapKey => $mapValue) {
$result[$mapKey] = $mapValue;
}
}
return new static($result);
});
Then run your Fractal results through it:
$results = collect($fractalResults)->fractal('camel_case')->get('data', []);
Is there any convenience method that allows me to concatenate two Doctrine ArrayCollection()? something like:
$collection1 = new ArrayCollection();
$collection2 = new ArrayCollection();
$collection1->add($obj1);
$collection1->add($obj2);
$collection1->add($obj3);
$collection2->add($obj4);
$collection2->add($obj5);
$collection2->add($obj6);
$collection1->concat($collection2);
// $collection1 now contains {$obj1, $obj2, $obj3, $obj4, $obj5, $obj6 }
I just want to know if I can save me iterating over the 2nd collection and adding each element one by one to the 1st collection.
Thanks!
Better (and working) variant for me:
$collection3 = new ArrayCollection(
array_merge($collection1->toArray(), $collection2->toArray())
);
You can simply do:
$a = new ArrayCollection();
$b = new ArrayCollection();
...
$c = new ArrayCollection(array_merge((array) $a, (array) $b));
If you are required to prevent any duplicates, this snippet might help. It uses a variadic function parameter for usage with PHP5.6.
/**
* #param array... $arrayCollections
* #return ArrayCollection
*/
public function merge(...$arrayCollections)
{
$returnCollection = new ArrayCollection();
/**
* #var ArrayCollection $arrayCollection
*/
foreach ($arrayCollections as $arrayCollection) {
if ($returnCollection->count() === 0) {
$returnCollection = $arrayCollection;
} else {
$arrayCollection->map(function ($element) use (&$returnCollection) {
if (!$returnCollection->contains($element)) {
$returnCollection->add($element);
}
});
}
}
return $returnCollection;
}
Might be handy in some cases.
$newCollection = new ArrayCollection((array)$collection1->toArray() + $collection2->toArray());
This should be faster than array_merge. Duplicate key names from $collection1 are kept when same key name is present in $collection2. No matter what the actual value is
You still need to iterate over the Collections to add the contents of one array to another. Since the ArrayCollection is a wrapper class, you could try merging the arrays of elements while maintaining the keys, the array keys in $collection2 override any existing keys in $collection1 using a helper function below:
$combined = new ArrayCollection(array_merge_maintain_keys($collection1->toArray(), $collection2->toArray()));
/**
* Merge the arrays passed to the function and keep the keys intact.
* If two keys overlap then it is the last added key that takes precedence.
*
* #return Array the merged array
*/
function array_merge_maintain_keys() {
$args = func_get_args();
$result = array();
foreach ( $args as &$array ) {
foreach ( $array as $key => &$value ) {
$result[$key] = $value;
}
}
return $result;
}
Add a Collection to an array, based on Yury Pliashkou's comment (I know it does not directly answer the original question, but that was already answered, and this could help others landing here):
function addCollectionToArray( $array , $collection ) {
$temp = $collection->toArray();
if ( count( $array ) > 0 ) {
if ( count( $temp ) > 0 ) {
$result = array_merge( $array , $temp );
} else {
$result = $array;
}
} else {
if ( count( $temp ) > 0 ) {
$result = $temp;
} else {
$result = array();
}
}
return $result;
}
Maybe you like it... maybe not... I just thought of throwing it out there just in case someone needs it.
Attention! Avoid large nesting of recursive elements. array_unique - has a recursive embedding limit and causes a PHP error Fatal error: Nesting level too deep - recursive dependency?
/**
* #param ArrayCollection[] $arrayCollections
*
* #return ArrayCollection
*/
function merge(...$arrayCollections) {
$listCollections = [];
foreach ($arrayCollections as $arrayCollection) {
$listCollections = array_merge($listCollections, $arrayCollection->toArray());
}
return new ArrayCollection(array_unique($listCollections, SORT_REGULAR));
}
// using
$a = new ArrayCollection([1,2,3,4,5,6]);
$b = new ArrayCollection([7,8]);
$c = new ArrayCollection([9,10]);
$result = merge($a, $b, $c);
Combine the spread operator to merge multiple collections, e.g. all rows in all sheets of a spreadsheet, where both $sheets and $rows are ArrayCollections and have a getRows(): Collection method
// Sheet.php
public function getRows(): Collection { return $this->rows; }
// Spreadsheet.php
public function getSheets(): Collection { return $this->sheets; }
public function getRows(): Collection
return array_merge(...$this->getSheets()->map(
fn(Sheet $sheet) => $sheet->getRows()->toArray()
));
Using Clousures PHP5 > 5.3.0
$a = ArrayCollection(array(1,2,3));
$b = ArrayCollection(array(4,5,6));
$b->forAll(function($key,$value) use ($a){ $a[]=$value;return true;});
echo $a.toArray();
array (size=6) 0 => int 1 1 => int 2 2 => int 3 3 => int 4 4 => int 5 5 => int 6