Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

mr-grayda's avatar

Recursive (Child > Parent) relationship?

I'm trying to figure out an elegant way of solving this issue, but my mind has drawn blanks each time:

Let's say I have a table called alerts. An Alert has three properties: A message, a serial, and a parent_serial. The idea being, an alert can be related to another alert via a parent_serial. For example:

[
  "message" => "Outage reported",
  "serial" => 1,
  "parent_serial" => null
],
[
  "message" => "Problem identified",
  "serial" => 2,
  "parent_serial" => 1
],
[
  "message" => "Outage resolved",
  "serial" => 3,
  "parent_serial" => 2
],
[
  "message" => "Something else",
  "serial" => 4,
  "parent_serial" => null
]

So when you get all the alerts with parents, you'd (want to) see something like:

[
  "message" => "Something else",
  "serial" => 4,
  "parents" => []
],
[
  "message" => "Outage Resolved",
  "serial" => 3,
  "parents" => [
    "message" => "Problem Identified",
    "serial" => 2,
    "parents" => [
      "message" => "Outage Reported",
      "serial" => 1,
      "parents" => []
    ]
  ]
]

Or in other words, I want to show the newest alert FIRST, then have the "parents" property (recursively) contain the related alerts.

This is easy enough to do by creating a recursive relationship:

    public function parents() {
      return $this->hasMany(Alert::class, 'serial', 'parent_serial')->with('parents');
    }

But then that leads to duplicates:

[
  "message" => "Something else",
  "serial" => 4,
  "parents" => []
],
[
  "message" => "Outage Reported",
  "serial" => 1,
  "parents" => []
],
[
  "message" => "Outage Resolved",
  "serial" => 3,
  "parents" => [
    "message" => "Problem Identified",
    "serial" => 2,
    "parents" => [
      "message" => "Outage Reported",
      "serial" => 1,
      "parents" => []
    ]
  ]
],
[
  "message" => "Problem Identified",
  "serial" => 2,
  "parents" => [
    "message" => "Outage Reported",
    "serial" => 1,
    "parents" => []
  ]
]

Or in other words:

[4, 3, 2, 1, [3 => [2 => [1]]], [2 => [1]]]

tl;dr: I want something similar to a nested comment system, but flipped on its head, so the newest comment is first, then nested within that newest comment is the next oldest comment, and within that, the next oldest, and so on.

Is there an elegant (Eloquent) solution for this? I thought about a foreach within a foreach (and possibly a foreach within that) but no matter what, I always feel there's a better way to do it.

0 likes
4 replies
Snapey's avatar

add a whereHas('parent') to the top level of the query so that the duplicates are ommitted

you might need something else to include alerts with no children

mr-grayda's avatar

@SNAPEY - Didn't seem to work for me.

function getAlerts() {
    return (new Alert)
        ->whereHas('parent')
        ->with('parent')
        ->get();
}

Doing this just returned similar data as before (obviously without the parent-less alerts), because alert 2 'has' a parent of 1, and 3 has a parent of 2

Snapey's avatar

so as you follow each alert to its parent how can you not have duplicated nodes?

That would require turning the tree on its head and using children instead of parents (still uses parent_id)

Perhaps you could illustrate how you would like it to be represented?

mr-grayda's avatar

@SNAPEY - Here's my current output from my app with some dummy data (ignore the extra fields like product_id, date etc., as they're just extra stuff I'm tracking):

[{
    "product_id": "ABC",
    "serial": 1,
    "parent_serial": null,
    "message": "An outage was detected",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": []
}, {
    "product_id": "ABC",
    "serial": 2,
    "parent_serial": 1,
    "message": "The cause of the outage was found. A fix is being put into place",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": [{
        "product_id": "ABC",
        "serial": 1,
        "parent_serial": null,
        "message": "An outage was detected",
        "expires_at": null,
        "active": true,
        "date": null,
        "request_date": "2019-06-29T13:19:44+00:00",
        "parent": []
    }]
}, {
    "product_id": "ABC",
    "serial": 3,
    "parent_serial": 2,
    "message": "The fix has been implemented. Testing commencing.",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": [{
        "product_id": "ABC",
        "serial": 2,
        "parent_serial": 1,
        "message": "The cause of the outage was found. A fix is being put into place",
        "expires_at": null,
        "active": true,
        "date": null,
        "request_date": "2019-06-29T13:19:44+00:00",
        "parent": [{
            "product_id": "ABC",
            "serial": 1,
            "parent_serial": null,
            "message": "An outage was detected",
            "expires_at": null,
            "active": true,
            "date": null,
            "request_date": "2019-06-29T13:19:44+00:00",
            "parent": []
        }]
    }]
}, {
    "product_id": "ABC",
    "serial": 4,
    "parent_serial": 3,
    "message": "The fix was successful. The outage was over",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": [{
        "product_id": "ABC",
        "serial": 3,
        "parent_serial": 2,
        "message": "The fix has been implemented. Testing commencing.",
        "expires_at": null,
        "active": true,
        "date": null,
        "request_date": "2019-06-29T13:19:44+00:00",
        "parent": [{
            "product_id": "ABC",
            "serial": 2,
            "parent_serial": 1,
            "message": "The cause of the outage was found. A fix is being put into place",
            "expires_at": null,
            "active": true,
            "date": null,
            "request_date": "2019-06-29T13:19:44+00:00",
            "parent": [{
                "product_id": "ABC",
                "serial": 1,
                "parent_serial": null,
                "message": "An outage was detected",
                "expires_at": null,
                "active": true,
                "date": null,
                "request_date": "2019-06-29T13:19:44+00:00",
                "parent": []
            }]
        }]
    }]
}, {
    "product_id": "ABC",
    "serial": 5,
    "parent_serial": null,
    "message": "Here's a random alert that has no child",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": []
}]

And here's how I want the data to actually be represented:

[{
    "product_id": "ABC",
    "serial": 4,
    "parent_serial": 3,
    "message": "The fix was successful. The outage was over",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": [{
        "product_id": "ABC",
        "serial": 3,
        "parent_serial": 2,
        "message": "The fix has been implemented. Testing commencing.",
        "expires_at": null,
        "active": true,
        "date": null,
        "request_date": "2019-06-29T13:19:44+00:00",
        "parent": [{
            "product_id": "ABC",
            "serial": 2,
            "parent_serial": 1,
            "message": "The cause of the outage was found. A fix is being put into place",
            "expires_at": null,
            "active": true,
            "date": null,
            "request_date": "2019-06-29T13:19:44+00:00",
            "parent": [{
                "product_id": "ABC",
                "serial": 1,
                "parent_serial": null,
                "message": "An outage was detected",
                "expires_at": null,
                "active": true,
                "date": null,
                "request_date": "2019-06-29T13:19:44+00:00",
                "parent": []
            }]
        }]
    }]
}, {
    "product_id": "ABC",
    "serial": 5,
    "parent_serial": null,
    "message": "Here's a random alert that has no child",
    "expires_at": null,
    "active": true,
    "date": null,
    "request_date": "2019-06-29T13:19:44+00:00",
    "parent": []
}]

See how in the first example the alert that has a serial of 1 appears 4 times? First by itself, then as the parent of 4 > 3 > 2, then as the parent of 3 > 2, then as the parent of 2.

What I want is for it to show just once, as the parent of 4 > 3 > 2.

I could do this if I did something like this (in pseudo-code):

  • Get all the alerts
  • For each alert, get the parent_serial
  • Find the alert where serial == parent_serial, and move (don't copy!) it there, taking note of the move
  • Start the process from the top of the list of alerts, moving un-moved alerts to their proper place
  • Repeat this until there are no more elements to move.

But I'm curious if there's another way to do it that I haven't considered, such as some combination of Laravel's diff or unique with another collection method.

Please or to participate in this conversation.