Правильнее через внедрение зависимости (в частности DI).
В конструктор нужно передавать интерфейс.
Простой пример, в зависимости от настроек нам нужно отправить сообщение или через email или просто записать в файл для отладки, тогда
interface MessageProviderInterface {
public function sendMessage($from, $to, $text);
}
class Email implements MessageProviderInterface{}
class File implements MessageProviderInterface{}
class NewService {
private MessageProviderInterface $provider;
public function __construct(MessageProviderInterface $provider) {
$this->provider = $provider;
}
}
$provider = getenv('ENV') === 'DEBUG' ? new FileProvider() : new EmailProvider();
$service = new NewService($provider);
Это буквы I и D в принципе SOLID.
DI контейнер просто позволяет проще регистрировать эти зависимости (не только но в основном)