5

I have to retrieve just an array of id from the given collection, something like [10,54,61,21,etc]. I've tried flatten, pluck, but nothing seems to work apart from a foreach which is something I would like to remove at this step.

// Model
class Children extends Eloquent {
    public function directChildrens(){
        return $this->hasMany('App\Children','father_id','id')->select('id','father_id');
    }

    public function childrens(){
        return $this->directChildrens()->with('childrens');
    }
}

// Controller
return $children->childrens()->get();

As expected it works fine. Here a result:

[{
"id": 10,
"father_id": 2,
"childrens": [
    {
        "id": 54,
        "father_id": 10,
        "childrens": [
            {
                "id": 61,
                "father_id": 54,
                "childrens": []
            }
        ]
    },
    {
        "id": 21,
        "father_id": 10,
        "childrens": []
    }
]
}]

How can I perform a pluck('id') of this collection in order to get [10,54,61,21] ?

Vixed
  • 3,429
  • 5
  • 37
  • 68
  • is there a possibility "the result array" to have more than one element ? – Ersoy Jun 01 '20 at 18:16
  • what do you mean @Ersoy? – Vixed Jun 01 '20 at 18:28
  • https://prnt.sc/srvidi according to the screenshot is there a possibility to have an another json object/element inside the array ? But i posted an answer, let me know if it works for your case. – Ersoy Jun 01 '20 at 18:30

6 Answers6

7
$result = [
            [
                "id" => 10,
                "father_id" => 2,
                "childrens" => [
                    [
                        "id" => 54,
                        "father_id" => 10,
                        "childrens" => [
                            [
                                "id" => 61,
                                "father_id" => 54,
                                "childrens" => []
                            ]
                        ]
                    ],
                    [
                        "id" => 21,
                        "father_id" => 10,
                        "childrens" => []
                    ]
                ]
            ]
        ];
return collect($result)
        ->map(function ($value) {
            return Arr::dot($value); // converts it to the dot notation version (added the example at the end)
        })
        ->collapse() // collapse them into a single one
        ->filter(function ($value, $key) {
            return $key === 'id' || Str::endsWith($key, '.id'); // filter first id + patterns of .id
        })
        ->values() // discard dot notation keys
        ->toArray();

Arr::dot() method turned collection into the following format which flattens all into the same level.

[
  {
    "id": 10,
    "father_id": 2,
    "childrens.0.id": 54,
    "childrens.0.father_id": 10,
    "childrens.0.childrens.0.id": 61,
    "childrens.0.childrens.0.father_id": 54,
    "childrens.0.childrens.0.childrens": [],
    "childrens.1.id": 21,
    "childrens.1.father_id": 10,
    "childrens.1.childrens": []
  }
]

Thanks to @Dan warning about deprecated/removed function array_dot. Replaced with Arr::dot()

Ersoy
  • 8,816
  • 6
  • 34
  • 48
  • 1
    I suggest using `Illuminate\Support\Arr::dot()` instead of the `array_dot` helper because since Laravel 5.8 these [helpers are deprecated](https://github.com/laravel/framework/pull/26898) and were eventually [removed all together](https://github.com/laravel/framework/pull/27504) in Laravel 5.9. – Dan Jun 07 '20 at 17:22
  • Thanks @Dan - didn't know that - i will update it accordingly. – Ersoy Jun 07 '20 at 17:23
3

You can call array_walk_recursive only once, I just wraped into a pipe method to use functional approach used in Eloquent and Collections

return $children->childrens()->get()->pipe(function ($collection) {
    $array = $collection->toArray();
    $ids = [];
    array_walk_recursive($array, function ($value, $key) use (&$ids) {
        if ($key === 'id') {
            $ids[] = $value;
        };
    });
    return $ids;
});

Note that you must convert collection to array and store it in a variable as array_walk_recursive first parameter is passed by reference, then the following code would cause fatal error Only variables should be passed by reference

array_walk_recursive($collection->toArray(), function (){
    // ...
}) 
Pedro Sanção
  • 1,328
  • 1
  • 11
  • 16
2

A recursive solution right here, requires to declare an additional method though:

function extractIds($obj)
{
    $result = [];

    if (isset($obj['id'])) {
        $result[] = $obj['id'];
    }

    if (isset($obj['children'])) {
        return array_merge($result, ...array_map('extractIds', $obj['children']));
    }

    return $result;
}

The method will securely check for the id property as well as the childrens property before recursing into it. (By the way, the word children is already the plural form, no need for an additional s at the end. So be aware I removed it in my example.)

Using this, we can utilize a Laravel collection to easily chain the required transformation calls:

$result = collect($array)
    ->map('extractIds')
    ->flatten()
    ->toArray();
Namoshek
  • 6,394
  • 2
  • 19
  • 31
2

Not sure you want pluck('id') mean but pluck doesn't works on recursive array. So i use recursive to get the id values.

$result->map(function ($item) {
  $ids = [];
  array_walk_recursive($item['childrens'], function ($item, $key) use(&$ids) {
     if($key === 'id') $ids[] = $item;
  });
  return array_merge([$item['id']], $ids);
});
bangnokia
  • 259
  • 2
  • 9
1

you can use flatten() to get an array of [id, father_id, id, father_id, ...]. then you make a filter to get just the values with even indexes:

$result = [
            [
                "id" => 10,
                "father_id" => 2,
                "childrens" => [
                    [
                        "id" => 54,
                        "father_id" => 10,
                        "childrens" => [
                            [
                                "id" => 61,
                                "father_id" => 54,
                                "childrens" => []
                            ]
                        ]
                    ],
                    [
                        "id" => 21,
                        "father_id" => 10,
                        "childrens" => []
                    ]
                ]
            ]
        ];


$collection = collect($result);

$flattened = $collection->flatten()->all();

return array_filter($flattened, function ($input) {return !($input & 1);}, ARRAY_FILTER_USE_KEY);

you can add a check of the first node if it had a father_id (in case of the head of tree), if not, you take the first id and execute this code for children.

0

Here is a recursive algorithm you may add into your Children model:

use Illuminate\Support\Collection;

public function getAllChildrenIds()
{
    return Collection::make([$this->getKey()])
        ->merge(
            $this->childrens->map->getAllChildrenIds()
        );
}
Shizzen83
  • 3,325
  • 3
  • 12
  • 32
  • Plucking `id` can't handle your multi-dimensional example – Shizzen83 May 26 '20 at 13:06
  • So, why you posted that answer? There is nothing form me there, unless you provide a real use of it. – Vixed May 26 '20 at 13:09
  • 1
    ` nothing seems to work apart from a foreach` I offered you another way than the `foreach`, which is primarily the goal you want to reach. Accepting it or not is up to you. – Shizzen83 May 26 '20 at 13:12