Как можно объединить условия трех зависимых сущностей в policy?
public function delete(User $user, Menu $menu) {
        // dd($menu->project->id, $menu->project_id);
        return $this->projectPolicy->isAdmin($user, $menu->project);
    }
Зачем вы так? Ну у вас уже есть пользователь. Есть модель Project. Нахрена вы тащите руками Policy. Ну определите разрешение на модификацию элементов меню в ProjectPolicy и используйте на здоровье.

public function delete(User $user, Menu $menu) {
        return $user->can('delete-menu', $menu->project);
}
Прежде чем читать ниже, обратите внимание на ответ Дмитрий. Четко. Понятно. По делу. Я дам теорию.

Контроль доступа тема немножко сложнее, чем просто по документации создать политики.

По большей части контроль доступа состоит из проверки разрешений перед совершением действия, и проверки политики перед выдачей данных. В ларавеле например есть только политики. В симфони есть только воутеры, которые с натяжкой округляются до разрешений.

Чтобы что-то сделать (удалить), вам нужно проверять доступы не к модели, а к действию. Вы их можете положить в модель или в политику, но как вы прикажете базе данных "не возвращать после SELECT записи в которых есть такой-то параметр", когда политика на вход ждет экземпляр модели, который уже получен из базы?

Политика это про "что можно видеть и что не можно".
Политика - фильтрует_список ИЛИ запрещает_действие_с_одной_записью (по факту - фильтрует одну запись до пустой переменной и дальнейшее выполнение не имеет смысла).
Разрешения - запрещают действие, и они отталкиваются часто от довольно сложных запросов, как-то проверить время, рабочий день, параметры среды, параметры десятка моделей и тд.

Что мы получаем в итоге. Если вы хотите канонично хранить все в политике - создайте внутри политики меню метод, возвращающий Eloquent\Builder или Query\Builder, который нужно выполнить. И не ожидайте что он выполнится "автоматически", вообще в контроле доступа автоматическая магия только вредит. Вызовите его в контроллере так, как вы вызываете $this->validate(), вы же не ожидаете что валидация "выполнится сама", хотя и могла бы, но это неочевидно для тех кто после вас код читать будет.

В итоге вы отсечете модель еще до того, как она пришла из базы данных по вашему критерию. Другой вопрос - если вы используете подкидывание на вход метода контроллере (известное как SubstituteBindings), то есть ваша модель тянется по айди, и ей наплевать на условности. В этом случае вам нужно как-то получить экземпляр политики в контроллере.

Вы либо в модели делаете метод newPolicy/getPolicy(), возвращающий политику, в которой есть метод, ожидающий на вход и ту модель и эту, и делающие все действия. В итоге это уже не про "как в ларавеле", это про обычный ООП - написали метод в политике, создали класс, вызвали, и про хранение "где удобно чтоб лежало".

В любом случае использование ::registerPolicies() которые автоматически что-то там проверяют, это неполноценный контроль доступа, тут заманаешься приделывать "канонично", схитрить можно, но проще разрешить себе проверки разрешений и параметров среды отдельно.

Я обычно пишу свой класс гейта, в который вообще передаю список разрешений (с поддержкой разных ИЛИ даже может быть) из реквеста, который там в статическом методе лежит, прогоняет эти разрешения. Если действие еще и зависит от запросов - я там же и запрос положу в методе, вызову его из реквеста, выполню, проверю результат. И только потом достану модель из БД, и продолжу что-то с ней делать.

А с помощью политик или обычных ифов я скорее скрою поля в ответе json. И даже тут можно настроить Serializer и группы, которые это будут делать на основе реквеста или же политики.

====

Вообще про контроль доступа много можно начитать, но я здесь вкратце дам то что вы там найдете. Есть подходы ACL, RBAC, ABAC и ещё какие-то менее известные.

ACL - это про "разрешения". У вас есть список действующих лиц, список ключей (разрешений), список ключниц (ролей), а в коде написаны замки (ифы), которые ключами открываются. В итоге ваш класс берет ключи на ключнице, и пытается открыть замок, если не открылось - бросает 403. Есть ли тут сложности? Есть. Пример был описан на одной из статей на хабре про звездолет. Наняли одного человека - он монтажник двигателя. Потом наняли второго, а первого переназначили на повара. У повара должны быть чистые руки, у механика - грязные. При этом права у них одинаковые (согласно их умений в жизни), но права повара - отключают некоторые права механика и наоборот. А потом мы добавим третью роль, которая например дает права обратно и всё, мы поплыли. Тут и появляется та штука, что мы встречали в apache - deny first / allow first. По мне deny first - безопаснее. Если есть хоть один запрет на полномочие, то можешь потом хоть сто раз его добавить - его не будет. Можно кидать другое исключение что-то вроде "полномочие запрещено".

RBAC - это придумка Yii (вернее в оригинале это концепт с ролями как выше написал, но реализовали его потом на PHP разработчики Yii, очень своеобразно), который попытался магически сделать ACL. Он основывался на том, что изначально у чего-то (модели, ресурса) есть методы проверки ключей, но чем "круче" роль пользователя (наследование), тем более злой метод применяется. Это жутко неудобно писать на самом деле, потому как наследование начинает путаться с композицией, а иерархия ролей ломаться, потому как постоянно хочется создать роль на основе существующей, и со временем хочется на основе двух существующих и вообще там такой лес начинается что просто беда.

ABAC - это попытка зайти вообще со стороны ООП. То есть вместо ролей, разрешений и прочего - предлагается использовать обычные функции, в которую кидать на вход всё, что требуется для проверки, а на выход выдавать "можно или нельзя". При этом эти функции могут ходить в базу, что-то там спрашивать, выполнять предварительные действия, то есть как бы "полная свобода", но вот минус - ПОЛНОЕ отсутствие стандарта и невозможность это объяснить. То есть "можно всё" как и в коде. Это и сделано в Симфони с вотерами, но тут нарощен ещё один кусок - вместо deny/allow есть ещё vote, чем больше проголосовало - тем больше вероятность, что "можно" или "нельзя".

При этом у лары здравая идея с политиками, которые точно нужны например, когда вы выдатаете пользователей и  баланс на их кошельках. При всем уважении баланс могут смотреть администраторы, менеджеры и сам юзер, но не его друзья (которые могут получить список юзеров под действием "дай список моих друзей"). И это вопрос даже не ролей, это вопрос наличия ключа "могу смотреть балансы". Обратите внимание, что модель при этом есть, но у нее не видны несколько полей. Технически можно запихнуть все методы ABAC и назвать это политикой, и это будет правильно с точки зрения "все доступы - в политиках" - но вот автоматическое их применение лара не дотянула до приемлемого уровня, а оно и не надо.

Так что контроль доступа это про "настроить все способы защиты и применять те, которые сейчас нужны по методике ABAC" - то есть в обычных функциях проверять хочешь ключи-разрешения, хочешь - роли, хочешь день недели, а хочешь - поле в модели.

Разумеется, контроль доступа чаще всего делают "настраиваемым в админке". Но попробуй-ка настроить в админке обычную функцию, которая может делать что-угодно. Вот и пришли к выводу, что в админке настраивают часть ACL, что называется дают разрешения в роли, вяжут роль к юзеру, а потом от этих разрешений зависит какие действия делает обычная функция. Некоторые даже делали flow-контроль, позволяющий чуть ли не в графическом редакторе строить логику добавляя "или", "и", "больше", "меньше" - но этим реально пользуются потом только программисты, никто не хочет ничего сломать, поэтому - в админке дают разрешения, а по ним написан код. Самый правильный способ.