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

YeYar's avatar
Level 1

'with' relations with exclusions

Hello!

Been struggling with this all day now and can't come up with a good solution. I can solve it with regular SQL, but I really want the neat structure you get back (Collection) when using the "with" function.

So I have these tables (the important ones for this problem).

Tiles

  • id
  • tile
  • tile_type_id
  • player_id
  • zone_id

Player_unit_groups

  • id
  • tile_id
  • player_id
  • name

Player_units

  • id
  • unit_it
  • player_unit_group_id
  • count

Units

  • id
  • name
  • defence
  • attack
  • type

Unit_events

  • id
  • event_type_id
  • tile_id
  • player_unit_group_id

And I can get out the data I want by doing the following.

>>> Tile::where('id', '224718')->with(['player_unit_groups.player_units.unit'])->first();

Which results in

=> App\Tile {#776
     tile: 2218,
     player_unit_groups: Illuminate\Database\Eloquent\Collection {#809
       all: [
         App\PlayerUnitGroup {#812
           id: 1,
           tile_id: 224718,
           player_id: 1,
           public_uuid: "a6b01d5f-cef7-4438-b476-de38a78ce695",
           name: "",
           created_at: "2017-11-13 13:26:53",
           updated_at: "2017-11-13 13:26:53",
           player_units: Illuminate\Database\Eloquent\Collection {#819
             all: [
               App\PlayerUnit {#820
                 id: 1,
                 unit_id: 1,
                 player_unit_group_id: 1,
                 count: 50,
                 public_uuid: "9bcd7281-1d5d-4fc1-af3d-1c0f3df5dafe",
                 created_at: "2017-11-13 13:26:53",
                 updated_at: "2017-11-13 13:26:53",
                 unit: App\Unit {#821
                   id: 1,
                   name: "Unit I",
                   defence: 3,
                   attack: 5,
                   type: 0,
                   created_at: "2017-11-13 13:25:13",
                   updated_at: "2017-11-13 13:25:13",
                 },
               },
             ],
           },
         },
       ],
     },
   }

what I want to do now, and what is the core of my problem, is that I want any PlayerUnitGroup that has a corresponding row in the "unit_events" table to not be returned, and excluded from the result. Any PlayerUnitGroup that does not appear in said table should be returned in the above result.

The reason the above data structure is important to me is because so far in my application I've been using this plus the "hidden" attribute of the models, to hide sensitive data and then just return the Collection as json to my client, which is super neat. So it would feel wrong if I had to do this with raw SQL and lose the structure.

Any ideas?

0 likes
3 replies
YeYar's avatar
Level 1

As an addition. If I try to refine the expression a bit.

Tile::where('id', '224718')
->with(['player_unit_groups.player_units' 
=> function($query)
{
    $query->join('units', 'units.id', 'player_units.unit_id');
}])->first();

I can get a nicer result.

=> App\Tile {#824
     tile: 2218,
     player_unit_groups: Illuminate\Database\Eloquent\Collection {#851
       all: [
         App\PlayerUnitGroup {#829
           id: 1,
           tile_id: 224718,
           player_id: 1,
           public_uuid: "a6b01d5f-cef7-4438-b476-de38a78ce695",
           name: "",
           created_at: "2017-11-13 13:26:53",
           updated_at: "2017-11-13 13:26:53",
           player_units: Illuminate\Database\Eloquent\Collection {#828
             all: [
               App\PlayerUnit {#848
                 id: 1,
                 unit_id: 1,
                 player_unit_group_id: 1,
                 count: 50,
                 public_uuid: "9bcd7281-1d5d-4fc1-af3d-1c0f3df5dafe",
                 created_at: "2017-11-13 13:25:13",
                 updated_at: "2017-11-13 13:25:13",
                 name: "Unit I",
                 defence: 3,
                 attack: 5,
                 type: 0,
               },
             ],
           },
         },
       ],
     },
   }

But if I try to do a join against the unit_events table, I end up empty. And I have checked and made sure that there's a corresponding row in the database.

Tile::where('id', '224718')
->with(['player_unit_groups.player_units'
 => function($query)
{
    $query->join('units', 'units.id', 'player_units.unit_id');
}, 
'player_unit_groups' 
=> function($query)
{
    $query->join(
        'unit_events', 
        'player_unit_groups.id', 
        'unit_events.player_unit_group_id');
}])->first();
=> App\Tile {#865
     tile: 2218,
     player_unit_groups: Illuminate\Database\Eloquent\Collection {#874
       all: [],
     },
   }

So perhaps my problem is in the join? Or maybe that is to be seen as an illegal/bad way of trying to solve it? In the case above I'm just trying to make the join work to see that at least the relationship works there. And if I can get that working I will try to negate that.

YeYar's avatar
Level 1

Sorry, another update.

Basically what I want to do, but in Eloquent (and with the above mentioned structure).

USE game;
SELECT * FROM player_unit_groups
INNER JOIN player_units
ON player_unit_groups.id = player_units.player_unit_group_id
INNER JOIN units
ON units.id = player_units.unit_id
WHERE NOT EXISTS 
(
    SELECT 1 
    FROM unit_events e
    WHERE e.player_unit_group_id = player_unit_groups.id
    AND e.event_types_id = 1
);

Now, this query does exactly what I want. EXCEPT that it does not come back with the super sexy data structure :(.

YeYar's avatar
Level 1

Right, so I solved it. If anyone wants to do the same, here's the final expression.

Tile::where('id', '224718')
    ->with(
        [
            'player_unit_groups.player_units' 
            => function($query) 
            {
                $query->join(
                    'units', 
                    'units.id', 
                    'player_units.unit_id');
            }, 
            'player_unit_groups' 
            => function($query) 
            {
                $query->whereNotExists(
                    function($query)
                    {
                        $query->select(DB::raw(1))
                            ->from('unit_events')
                            ->whereRaw('unit_events.player_unit_group_id = player_unit_groups.id')
                            ->whereRaw('unit_events.event_types_id = 1');
                    });
            }])->first();

Suggestions on how to make it even better is greatly appreciated :-)

Please or to participate in this conversation.