phparraysarray-reduce

Create a new array with key as value of old array with total count?


I have a array with following values.I am trying to create a new array using array php array functions and trying to max avoid foreach. The key we are using for new array is "status" and depending on status we make new array for each mail id.

<?php
[
    {
        "mail_id": "29848947",
        "last_name": "Doe",
        "first_name": "Jon",
        "email": "jdoe@gmail.com",
        "status": "opened"
    },
    {
        "mail_id": "340980398",
        "last_name": "Doe",
        "first_name": "Jane",
        "email": "janedoe@gmail.com",
        "status": "sent"
    },
    {
        "mail_id": "877586",
        "last_name": "Dwaye",
        "first_name": "Jhon",
        "email": "Jhondw@yahoo.com",
        "status": "clicked"
    },
    {
        "mail_id": "225253463",
        "last_name": "Doe",
        "first_name": "Jon",
        "email": "jdoe@gmail.com",
        "status": "opened"
    },
    {
        "mail_id": "849849w4",
        "last_name": "Doe",
        "first_name": "Jane",
        "email": "janedoe@gmail.com",
        "status": "sent"
    }
]
?>

Result or new array as below. I am trying to achieve the below result using any array function like , array_walk_recursive or array_reduce that makes the code look beautiful and compact.

<?php
 [
    [
            "first_name": "Jon",
            "last_name": "Doe",
            "email": "jdoe@gmail.com",
            "opened": 2,
            "blocked": 0,
            "hard_bounced": 0,
            "soft_bounced": 0,
            "received": 0,
            "clicked": 0
    ],
    [
            "first_name": "Jane",
            "last_name": "Doe",
            "email": "janedoe@gmail.com",
            "opened": 0,
            "blocked": 0,
            "hard_bounced": 0,
            "soft_bounced": 0,
            "sent": 2,
            "clicked": 0
    ],
    [
        "first_name": "Jhon",
        "last_name": "Dwaye",
        "email": "Jhondw@yahoo.com",
        "opened": 0,
        "blocked": 0,
        "hard_bounced": 0,
        "soft_bounced": 0,
        "sent": 0,
        "clicked": 1
    ],
]

Solution

  • Using array_reduce

    Using array_reduce is likely your best bet, as you guessed. It is sort of thinking through this as a loop, without using foreach explicitly. Here's my solution, I think this is pretty compact for what you're looking to accomplish.

    $result = array_values(array_reduce($source, function($carry, $event) {
        if(!array_key_exists($event['email'], $carry)) {
            $carry[$event['email']] = [
                "first_name" => $event["first_name"],
                "last_name" => $event["last_name"],
                "email" => $event["email"],
                "opened" => 0,
                "blocked" => 0,
                "hard_bounced" => 0,
                "sent" => 0,
                "clicked" => 0
            ];
        }
    
        $carry[$event['email']][$event["status"]]++;
    
        return $carry;
    }, []));
    

    Working example: https://3v4l.org/lhlU0


    Using array_map

    I did take a stab at another solution, just as an exercise. It's not as clean and compact as array_reduce, but sometimes it can be worth at least considering a non-loop approach.

    $result = array_map(function($email) use($source) {
        $events = array_values(array_filter($source, function($event) use($email) {
            return $event['email'] == $email;
        }));
    
    
        return [
            "first_name" => $events[0]["first_name"],
            "last_name" => $events[0]["last_name"],
            "email" => $email,
            "opened" => count(array_filter($events, function($event) { return $event["status"] == "opened"; })),
            "blocked" => count(array_filter($events, function($event) { return $event["status"] == "blocked"; })),
            "hard_bounced" => count(array_filter($events, function($event) { return $event["status"] == "hard_bounced"; })),
            "soft_bounced" => count(array_filter($events, function($event) { return $event["status"] == "soft_bounced"; })),
            "sent" => count(array_filter($events, function($event) { return $event["status"] == "sent"; })),
            "clicked" => count(array_filter($events, function($event) { return $event["status"] == "clicked"; })),
        ];
    }, array_unique(array_column($source, "email")));
    

    Working example: https://3v4l.org/KSGeX

    Though I would argue that those count(array_filter(... calls should be abstracted out to a separate function:

    function countEvents($events, $status) {
        return count(array_filter($events, function($event) use($status) { 
            return $event["status"] == $status; 
        }));
    }
    

    So now in the above return array you can just countEvents($events, "opened") for example. Will make it a good deal cleaner.