Я когда-то задал тот же самый вопрос и нашел на него ответ.
Для начала "private/protected/public" это больше история про наследование, чем про "доступ" или про "капсуляцию". Тут просто эти три слова несколькой обязанностей выполняют.
Сразу для тех кто "ненавидит наследование" и пока не знает "почему" он его ненавидит. Существует композиция, когда в один класс на вход конструктора передается другой. Так можно "наследовать" сколько хочешь функционала неограниченно раз, и хоть несколько классов, поэтому наследование это скорее про изменение написанного функционала в будущем (к тому же наследование делать немного быстрее чем композицию, и когда предполагается с десяток классов чего-то похожего, да ещё они простые - то почему нет), а НЕ про добавление нового путем "этот будет делать всё что и родитель, но ещё вот это". Так можно, но только если очень лень делать, как "нужно".
"публичный" метод говорит о том, что входящие данные нужно внутри проверять, потому что может вызвать кто-угодно, и он бы не хотел, чтобы оно свалилось в середине выполнения, причем неожиданно.
"защищенный" метод предполагает "внутренний функционал", который как правило работает с уже проверенными данными и выполняет некую работу, но что более важно - наследуется. То есть это "внутрянка которая является частью основной задачи класса" - настолько является, что если класс этот кто-то захочет поменять - вполне возможно, ему захочется поменять это действие но так, чтобы остальное то же работало. Разумеется стоит помнить о договоренностях разработчиков на уровне "ты нормальный, вообще?" - если класс возвращал квадрат, а после наследования начинает возвращать круг - кто-то может с этого потом изрядно удивиться. Про это принцип подстановки Лисков.
"приватный" же метод это про некую приблуду, которая "сегодня так сделана", а через три месяца "можно и вот так сделать". То есть это своего рода твои эксперименты, которые заработали, но ты бы не хотел, чтоб они в истории тянулись и кто-то потом в этом ковырялся, написал, работает, забыл. Почему я делю именно так - потому как если большую часть методов сделать приватными как делает доктрина например, то когда тебе кусок функционала не нравится ты тратишь недели на переписывание исправление и разбор ненужной чуши, тогда как чаще всего режим "поправить" преполагает замену 1-2 строк по хорошему, а вместо этого целый класс переписывать, потому что один приватник вызывает другой приватник и далее далее далее.
Теперь про свойства.
"Публичные" свойства как во всех языках нужны, чтобы просто к ним обращаться и вписывать туда значение. Плюсы как бы - обычная переменная, меньше кода писать. Минусы? Обьекты постоянны от функции к функции. Где-то на уровне 10м-20м кто-то всунет в этот обьект то, что ты бы не хотел там видеть и оно всунется, а ты потом офигеешь, что "а че, так можно было чтоли?" - да. То есть публичные свойства стоит использовать только в каких-то может одноразовых объектах, типа DTO/ValueObject паттерн, просто потому что от "защиты" там только число кода добавится, они просто как массивы только с именованными заранее полями. Удобно писать, редактор подсказки показывает. И все равно остается опасность того что выше написано, поэтому как бы предполагается что вот дто он такой, одноразовый, и пишут в него только в начале приложения, разбирая запрос на куски, запихнул что не надо - получай.
"Защищенные/Приватные" свойства здесь выполняют почти ту же задачу, что и защищенные методы (про наследование). Дополнительная фишка - в защищенное свойство ты можешь методами написать КАК ИМЕННО ты кладешь туда данные или всобачить небольшую проверку. Не стоит увлекаться тысячей проверок - в коде утонешь, но проверку Типа - можно, почему нет. Раньше типизированных свойств не было, так это был вообще единственный способ убедится что в поле лежит нужного класса другой объект. Я вот раньше писал set()/get()/delete()/add()/put() и кучу всего пока не понял, что можно обобщить до двух set(?$items) и add($items), причем set() - вызовет add() и если туда null передать, то оно либо поставит по умолчанию либо очистит, то есть ещё и delete() выполняет когда нужно.
Ещё кое-что про свойства. Стоит помнить, что в свойства можно закинуть любую информацию, в том числе скаляры. Скаляры это те типы, что копируются каждый раз, когда ты их присваиваешь в другую переменную потом. То есть увлекаться большим числом свойств никогда нельзя, это чревато тем, что при копировании обьектов ты скопируешь все данные внутри, внезапно увеличив потребление ресурсов компьютера. Конечно, на практике так очень редко бывает, что кто-то в свойство положит массив на 100 тысяч свойств, а потом клонирует обьект случайно в цикле десяток раз... Но где минус там и плюс - если объект не копировать - это пачка данных, которые хранятся один раз, а работают потом везде, так сказать общее хранилище.
Это не значит что есть норма "сколько", это значит что нужно понимать, что в свойствах большой обьем желательно не копить. Все поля имеющие "массив по-умолчанию" можно сделать методами, тогда вызов метода даст тебе данные в одной функции, а по завершению - их вычистит. А положить их в свойства и запомнить навсегда (это же удобно! а ещё быстро считывается!) - это печально.