Бросать исключение или возвращать коды ошибок/успеха?
Бросать исключение.
Является ли исключением то, что метод не может выполнить свою задачу?
Конечно же.
Но узнать является ли сумма(amount) корректной по-хорошему можно только в браузере/GUI или в Domain слое в самом методе transferMoney().
К фронту не должно быть доверия, данные обязательно нужно проверять.
Аргументы метода лучше проверять И на тип И на граничные значения. Например ваш amount по идее должен быть float + больше, или равен 0. Если amount таковым не является - бросайте исключение. Логика выше должна была это отсечь, еще на этапе валидации данных запроса.
В transferMoney() нужно извлечь данные пользователя(который пересылает деньги) из БД и проверить есть ли у него такая сумма.
Если ваш метод непосредственно имеет дело с кошельком пользователя - проверка в нем должна быть обязательно. Если же этот метод - только посредник, подобную проверку можно сделать в другом месте.
Получается в методе transferMoney() есть одна причина неудачи, а что если добавится ещё одна причина, например, пользователю запрещено временно пересылать деньги?
Тот же комментарий, что и про баланс.
Поскольку они ожидаемые, то бросать исключения не логично.
Почему же? Логика выше сможет обработать ваши ожидаемые исключения и через множественный catch определить, что не так. Это вполне нормально, и даже удобно.
В случае если бы была одна причина неудачи, то можно было бы просто вернуть false.
Очень хреновая практика. Как вы определите в вызывающем коде, что пошло не так? Что "false"? Баланса не хватает, нельзя делать покупки, или Меркурий в ретрограде?
возвращать что-то на подобии [false, $error] и [true, null]
Для php - это очень кривой подход. По двум причинам:
1. Вы расширяете интерфейс метода, просто так.
2. Вы нагружаете вызывающий код дополнительными обвязками проверок, опять же просто так.
В этом нет смысла так как есть механизм try-catch, который отлично решает это задачу.
Вот пример, допустим ваш transferMoney($amount, User $recipient) должен возвращать объект транзакции, пусть его сигнатура будет:
transferMoney(float $amount, User $recipient): Transaction
Вызывающий код знает, что он обязан в конце получить транзакцию, а если что-то пойдет не так - будет исключение, нет смысла в дополнительных проверках, а что если первый элемент массива false, а что если он true, но второй элемент не null, а что если второй элемент не тот, что ожидалось, а что если первый элемент - строка, и т.д.
transferMoney() не должен подстраиваться под слой повыше, в нём не должно быть никаких предположений, что пользователь введёт что-то правильно или неправильно, он просто должен ожидать только валидные данные, если они неправильные, то это исключительная ситуация.
Верно
То есть transferMoney() выбросит исключение, которое подымется до Controller'a, и на основе которого Controller отправит 400 Bad Request и причину неудачи.
Угу
Или всё таки первый подход к проблеме лучше(первый абзац)?
Первый подход - это путь боли, ошибок и отчаянья.
Вот вам еще чтива:
https://github.com/index0h/php-conventions