O que você está falando aqui é inversão de dependência e encapsulamento. Você está consumindo código quer manter uma dependência de algum meio de se apossar de entidades de domínio, sem precisar saber como isso acontece. Ele quer um conjunto de métodos em uma caixa preta, que espera fornecer algumas entradas e obter algumas saídas: uma interface.
Em vez de ter seu código dependente de alguma implementação concreta de acesso a dados, você depende de uma abstração: a interface. Contanto que seu código de chamada saiba apenas sobre a abstração, não importa como você preenche essa interface, contanto que você a faça corretamente.
Analisando os exemplos do Doctrine e do PDO bruto da sua pergunta, você pode definir uma interface como:
interface FooRepositoryInterface {
public function getAFoo($id);
}
Com a interface pronta, você pode implementá-la da maneira que achar melhor:
public class DoctrineFoo implements FooRepositoryInterface {
public function __construct(EntityManager $em){ ... }
public function getAFoo($id){
return $this->em->find("\Entity\Foo", $id);
}
}
public class PDOFoo implements FooRepositoryInterface {
public function __construct(PDO $pdo){ ... }
public function getAFoo($id){
$pdo->prepare("sql ...");
$row = $pdo->fetchOne();
return $this->makeFooFromRow($row);
}
}
Isso permite que seus outros módulos dependam alegremente do acesso aos dados em termos da interface:
class FooBazer {
public function doBaz(FooRepositoryInterface $repository, $id) {
$foo = $repository->getAFoo($id);
$foo->baz();
}
}