Вопрос: Почему Service Locator это зло и что использовать вместо?

Лучший ответ:
Все эти страшные слова - они на самом деле всегда про одно и то же - про связность. Когда ты хардкодишь внутри класса вызов какого-то конкретного сервиса - ты намертво к нему привязываешься. И чтобы поменять сервис на другой, ты будешь вынужден поменять код класса. Окей, поменял. И тут же в другом месте, где этот же класс использовался, что-то сломалось! И что теперь? Делать два класса, которые различаются одной строчкой? Нет конечно.  А как тогда использовать один и тот же класс для обработки разных входящих данных (или одних и тех же данных, но разными способами)? Сделать его поведение изменяемым. То есть сделать изменяемыми те инструменты, которыми он пользуется - т.е. его зависимости. 

Поэтому все зависимости обычно передаются через конструктор (и поэтому и называются инъекция зависимостей.) 

Таким образом мы можем менять поведение класса, не меняя его код

Но тут надо понимать, что всё это работает только при правильном применении ООП. А точнее просто при применении ООП. Потому что 98% "ООП" кода, который пишется на РНР - это голимая процедурщина, даже если она обёрнута в классы и методы. Если у тебя метод класса представляет из себя стену кода, которую ты тупо перенёс из файла, инклюдившегося в любимое похапешное спагетти - то это не ООП. Это та же процедурщина, вид сбоку. И смысл использования dependency injection ты с ним не почуствуешь. Будешь конечно применять, но в качестве карго культа - потому что тебе это на тостере написали. 
А вот когда твой код начнет становиться действительно объектным - тогда стразу станет понятнее. 

Похожим на сервис локатор является сервис- или DI-контейнер. Используемый вручную, он является тем же самым сервис локатором. Поэтому вручную его никогда не надо вызывать - что и запрещается в симфоневских конроллерах - а только для автоматического создания классов. В МВЦ у тебя ведь очень многие объекты создаются автоматом - сущности, контроллеры. И вот для того, чтобы при автоматическом создании экземпляра класса у тебя были на руках все требуемые сервисы - и нужен контейнер.

Соотвтственно, ответ на вопрос "что использовать?" очень простой:
- при ручном создании экземпляра объекта, все зависимости передавать в него через конструктор, а не получать "из воздуха" в коде.
- при автоматическом создании экземпляра объекта, использовать dependency injection container

В этим смысле очень полезно освоить Симфони - строгий фрейворк, в котором нет сервис локатора и в котором запрещено пользоваться контейнером напрямую.

Худший ответ:
1. Вы всё верно поняли. Сложность в том что внутри может оказаться не та структура которую вы ожидайте (ожидалась) 

Но это не значит что это плохо. Эту проблему можно решить валидацией. В вашем примере достаточно проверять на instanceOf. Тогда код станет правильный. А в symfony сделали это ещё лучше и добавили строгость к типам для сервис локатора через подписку на сервисы.

Что бы понять когда применять сервис локатор, а когда инъекцию пройдите по ссылке https://symfony.com/doc/current/service_container/... и почитайте.

Там есть пояснение:
Sometimes, a service needs access to several other services without being sure that all of them will actually be used. In those cases, you may want the instantiation of the services to be lazy. However, that’s not possible using the explicit dependency injection since services are not all meant to be lazy

Можно пояснить на примере: допустим у вас есть класс с методом, и внутри метода только в некоторых случаях вам понадобится сделать запись в БД (допустим  обращение к сервису в виде get->('db')). 

Например:
if($userData = $request->get('user_data')) {
    $db = $serviceLocator->get('db')->insert($userData);
} else {
   die("user data empty");
}

Если делать через инъекцию, то $db всегда будет инициализирована, например если проводить инъекцию через конструктор или метод, то объект вашего класса всегда будет при любых условиях запускать код инициализации $db и тратить на это ресурсы. Пример через инъекцию (тут тип инъекции через аргумент метода, аналогично можно сделать через конструктор или сеттер)

class MyClass {
   // $db будет инициализрован, но используется только в 1 случае для if
  // если $db тяжёлый сервис и инициализируется долго это проблема!
   function httpResponse(Request $request, Database $db) {
      
       if($userData = $request->get('user_data')) {
          $db->insert($userData);
       } else {
          die("user data empty");
       }
   }
}

А в случае сервис локатора $db будет инициализирован только в условии когда придёт user_data. Таким образом если у вас контроллер использует 15 сервисов (но это bad code!), а одновременно нужен только 1 из них, лучше использовать сервис локатор.

class MyClass {
   // В таком случае рекомендуется воспользоваться сервис локатором! 
   function httpResponse(Request $request, Database $db, Servive1 $s1, Service2 $s2) {
      
       if($userData = $request->get('user_data')) {
          $db->insert($userData);
          if ($db->lastId()) {
               $s1->makeSuccess();
               if($s1->isSuccess()) {
                   $s2->commit();
               }
          }
       } else {
          $s2->rollback();
          die("user data empty");
       }
   }
}

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

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

P.S:
Использовать service locator можно и в этом нет ничего страшного, особенно когда есть понимание что это такое и для чего он нужен.