repositorio query patron generic deletewhere php database laravel repository repository-pattern

php - patron - laravel repository query



¿Diseño apropiado del patrón del repositorio en PHP? (7)

Prefacio: Estoy intentando usar el patrón de repositorio en una arquitectura MVC con bases de datos relacionales.

Recientemente, comencé a aprender TDD en PHP y me doy cuenta de que mi base de datos está muy relacionada con el resto de mi aplicación. He leído acerca de los repositorios y el uso de un contenedor IoC para "inyectarlo" en mis controladores. Cosas muy interesantes. Pero ahora tienen algunas preguntas prácticas sobre el diseño del repositorio. Considere el siguiente ejemplo.

<?php class DbUserRepository implements UserRepositoryInterface { protected $db; public function __construct($db) { $this->db = $db; } public function findAll() { } public function findById($id) { } public function findByName($name) { } public function create($user) { } public function remove($user) { } public function update($user) { } }

Problema # 1: Demasiados campos

Todos estos métodos de búsqueda utilizan un enfoque de seleccionar todos los campos ( SELECT * ). Sin embargo, en mis aplicaciones, siempre trato de limitar la cantidad de campos que recibo, ya que esto a menudo aumenta los gastos generales y ralentiza las cosas. Para aquellos que usan este patrón, ¿cómo lidiar con esto?

Problema # 2: Demasiados métodos

Si bien esta clase se ve bien en este momento, sé que en una aplicación del mundo real necesito muchos más métodos. Por ejemplo:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

Como puede ver, podría haber una lista muy, muy larga de métodos posibles. Y luego, si agrega el problema de selección de campo anterior, el problema empeora. En el pasado, normalmente solía poner toda esta lógica en mi controlador:

<?php class MyController { public function users() { $users = User::select(''name, email, status'')->byCountry(''Canada'')->orderBy(''name'')->rows() return View::make(''users'', array(''users'' => $users)) } }

Con mi enfoque de repositorio, no quiero terminar con esto:

<?php class MyController { public function users() { $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name(''Canada''); return View::make(''users'', array(''users'' => $users)) } }

Problema # 3: Imposible emparejar una interfaz

Veo el beneficio de usar interfaces para repositorios, por lo que puedo intercambiar mi implementación (para propósitos de prueba u otros). Mi comprensión de las interfaces es que definen un contrato que debe seguir una implementación. Esto es excelente hasta que comience a agregar métodos adicionales a sus repositorios como findAllInCountry() . Ahora necesito actualizar mi interfaz para que también tenga este método, de lo contrario, otras implementaciones pueden no tenerlo, y eso podría romper mi aplicación. Por esto se siente una locura ... un caso de la cola moviendo al perro.

Patrón de especificación?

Esto me lleva a creer que el repositorio solo debería tener un número fijo de métodos (como save() , remove() , find() , findAll() , etc.). Pero entonces, ¿cómo ejecuto búsquedas específicas? He oído hablar del Patrón de especificación , pero me parece que esto solo reduce un conjunto completo de registros (a través de IsSatisfiedBy() ), que claramente presenta importantes problemas de rendimiento si se extrae de una base de datos.

¿Ayuda?

Claramente, necesito repensar un poco las cosas cuando se trabaja con repositorios. ¿Alguien puede aclarar cómo se maneja mejor?


Agregaré un poco sobre esto ya que actualmente estoy tratando de entender todo esto por mí mismo.

# 1 y 2

Este es un lugar perfecto para que su ORM haga el trabajo pesado. Si está utilizando un modelo que implementa algún tipo de ORM, solo puede usar sus métodos para cuidar estas cosas. Haga sus propias funciones orderBy que implementan los métodos de Eloquent si es necesario. Usando Eloquent por ejemplo:

class DbUserRepository implements UserRepositoryInterface { public function findAll() { return User::all(); } public function get(Array $columns) { return User::select($columns); }

Lo que parece estar buscando es un ORM. No hay razón para que su Repositorio no pueda basarse en uno. Esto requeriría una extensión elocuente del Usuario, pero personalmente no lo veo como un problema.

Sin embargo, si quiere evitar un ORM, tendrá que "rodar por su cuenta" para obtener lo que está buscando.

# 3

Las interfaces no deben ser requisitos duros y rápidos. Algo puede implementar una interfaz y agregarla. Lo que no puede hacer es fallar en implementar una función requerida de esa interfaz. También puede extender interfaces como clases para mantener las cosas en seco.

Dicho esto, estoy empezando a entender, pero estas realizaciones me han ayudado.


Estas son algunas soluciones diferentes que he visto. Hay ventajas y desventajas para cada uno de ellos, pero es decisión suya.

Problema # 1: Demasiados campos

Este es un aspecto importante, especialmente cuando se toma en cuenta las exploraciones de solo índice . Veo dos soluciones para hacer frente a este problema. Puede actualizar sus funciones para tomar un parámetro de matriz opcional que contendría una lista de columnas para devolver. Si este parámetro está vacío, devolverá todas las columnas de la consulta. Esto puede ser un poco raro; basado en el parámetro podría recuperar un objeto o una matriz. También puede duplicar todas sus funciones para que tenga dos funciones distintas que ejecuten la misma consulta, pero una devuelve una matriz de columnas y la otra devuelve un objeto.

public function findColumnsById($id, array $columns = array()){ if (empty($columns)) { // use * } } public function findById($id) { $data = $this->findColumnsById($id); }

Problema # 2: Demasiados métodos

Trabajé brevemente con Propel ORM hace un año y esto se basa en lo que puedo recordar de esa experiencia. Propel tiene la opción de generar su estructura de clase basada en el esquema de base de datos existente. Crea dos objetos para cada tabla. El primer objeto es una larga lista de funciones de acceso similares a las que usted ha enumerado actualmente; findByAttribute($attribute_value) . El siguiente objeto hereda de este primer objeto. Puede actualizar este objeto secundario para incorporar sus funciones de obtención más complejas.

Otra solución sería usar __call() para asignar funciones no definidas a algo accionable. Su método __call sería capaz de analizar el findById y findByName en diferentes consultas.

public function __call($function, $arguments) { if (strpos($function, ''findBy'') === 0) { $parameter = substr($function, 6, strlen($function)); // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0] } }

Espero que esto ayude al menos algo de qué.


Pensé que me tomaría un poco en responder mi propia pregunta. Lo que sigue es solo una forma de resolver los problemas 1-3 en mi pregunta original.

Descargo de responsabilidad: es posible que no siempre use los términos correctos cuando describo patrones o técnicas. Lo siento por eso.

Los objetivos:

  • Cree un ejemplo completo de un controlador básico para ver y editar Users .
  • Todo el código debe ser completamente comprobable y simulable.
  • El controlador no debe tener idea de dónde se almacenan los datos (lo que significa que se pueden cambiar).
  • Ejemplo para mostrar una implementación SQL (la más común).
  • Para obtener el máximo rendimiento, los controladores solo deben recibir los datos que necesitan, sin campos adicionales.
  • La implementación debe aprovechar algún tipo de mapeador de datos para facilitar el desarrollo.
  • La implementación debe tener la capacidad de realizar búsquedas complejas de datos.

La solución

Estoy dividiendo mi interacción de almacenamiento persistente (base de datos) en dos categorías: R (Lectura) y CUD (Crear, Actualizar, Eliminar). Mi experiencia ha sido que las lecturas son realmente lo que hace que una aplicación se ralentice. Y aunque la manipulación de datos (CUD) es en realidad más lenta, ocurre con menos frecuencia y, por lo tanto, es mucho menos preocupante.

CUD (Crear, Actualizar, Eliminar) es fácil. Esto implicará trabajar con models reales, que luego se pasan a mis Repositories para persistencia. Tenga en cuenta que mis repositorios seguirán proporcionando un método de lectura, pero simplemente para la creación de objetos, no se muestran. Más sobre eso más adelante.

R (Leer) no es tan fácil. No hay modelos aquí, solo objetos de valor . Utilice matrices si lo prefiere . Estos objetos pueden representar un solo modelo o una combinación de muchos modelos, cualquier cosa realmente. Estos no son muy interesantes por sí mismos, pero la forma en que se generan es. Estoy usando lo que estoy llamando Query Objects .

El código:

Modelo de usuario

Comencemos simple con nuestro modelo de usuario básico. Tenga en cuenta que no hay ORM extendido o cosas de base de datos en absoluto. Sólo pura gloria modelo. Agregue sus captadores, instaladores, validación, lo que sea.

class User { public $id; public $first_name; public $last_name; public $gender; public $email; public $password; }

Interfaz de repositorio

Antes de crear mi repositorio de usuarios, quiero crear mi interfaz de repositorio. Esto definirá el "contrato" que deben seguir los repositorios para que mi controlador pueda utilizarlo. Recuerde, mi controlador no sabrá dónde se almacenan realmente los datos.

Tenga en cuenta que mis repositorios solo contendrán estos tres métodos. El método save() es responsable de crear y actualizar usuarios, simplemente dependiendo de si el objeto de usuario tiene un conjunto de ID.

interface UserRepositoryInterface { public function find($id); public function save(User $user); public function remove(User $user); }

Implementación de repositorio SQL

Ahora para crear mi implementación de la interfaz. Como se mencionó, mi ejemplo iba a ser con una base de datos SQL. Tenga en cuenta el uso de un asignador de datos para evitar tener que escribir consultas SQL repetitivas.

class SQLUserRepository implements UserRepositoryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function find($id) { // Find a record with the id = $id // from the ''users'' table // and return it as a User object return $this->db->find($id, ''users'', ''User''); } public function save(User $user) { // Insert or update the $user // in the ''users'' table $this->db->save($user, ''users''); } public function remove(User $user) { // Remove the $user // from the ''users'' table $this->db->remove($user, ''users''); } }

Interfaz de objetos de consulta

Ahora, con CUD (Crear, Actualizar, Eliminar) atendido por nuestro repositorio, podemos centrarnos en la R (Leer). Los objetos de consulta son simplemente una encapsulación de algún tipo de lógica de búsqueda de datos. No son constructores de consultas. Al abstraerlo como nuestro repositorio, podemos cambiar su implementación y probarlo más fácilmente. Un ejemplo de un objeto de consulta puede ser un AllUsersQuery o AllActiveUsersQuery , o incluso MostCommonUserFirstNames .

Puede estar pensando "¿no puedo crear métodos en mis repositorios para esas consultas?" Sí, pero aquí es por qué no estoy haciendo esto:

  • Mis repositorios son para trabajar con objetos modelo. En una aplicación del mundo real, ¿por qué necesitaría obtener el campo de la password si quiero enumerar a todos mis usuarios?
  • Los repositorios a menudo son específicos del modelo, pero las consultas a menudo involucran más de un modelo. Entonces, ¿en qué repositorio pones tu método?
  • Esto mantiene mis repositorios muy simples, no una clase abultada de métodos.
  • Todas las consultas ahora están organizadas en sus propias clases.
  • Realmente, en este punto, los repositorios existen simplemente para abstraer la capa de mi base de datos.

Para mi ejemplo, crearé un objeto de consulta para buscar "AllUsers". Aquí está la interfaz:

interface AllUsersQueryInterface { public function fetch($fields); }

Implementación de objetos de consulta

Aquí es donde podemos usar un mapeador de datos nuevamente para ayudar a acelerar el desarrollo. Observe que estoy permitiendo un ajuste al conjunto de datos devuelto: los campos. Esto es todo lo que quiero con la manipulación de la consulta realizada. Recuerda, mis objetos de consulta no son constructores de consultas. Simplemente realizan una consulta específica. Sin embargo, dado que sé que probablemente lo esté usando mucho, en varias situaciones diferentes, me estoy dando la posibilidad de especificar los campos. ¡Nunca quiero devolver campos que no necesito!

class AllUsersQuery implements AllUsersQueryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function fetch($fields) { return $this->db->select($fields)->from(''users'')->orderBy(''last_name, first_name'')->rows(); } }

Antes de pasar al controlador, quiero mostrar otro ejemplo para ilustrar lo poderoso que es esto. Tal vez tengo un motor de informes y necesito crear un informe para AllOverdueAccounts . Esto podría ser complicado con mi asignador de datos, y es posible que desee escribir algo de SQL real en esta situación. No hay problema, aquí está el aspecto de este objeto de consulta:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface { protected $db; public function __construct(Database $db) { $this->db = $db; } public function fetch() { return $this->db->query($this->sql())->rows(); } public function sql() { return "SELECT..."; } }

Esto mantiene muy bien toda mi lógica para este informe en una clase, y es fácil de probar. Puedo burlarme del contenido de mi corazón, o incluso usar una implementación diferente por completo.

El controlador

Ahora la parte divertida: reunir todas las piezas. Tenga en cuenta que estoy usando la inyección de dependencia. Normalmente, las dependencias se inyectan en el constructor, pero en realidad prefiero inyectarlas directamente en mis métodos de control (rutas). Esto minimiza el gráfico de objetos del controlador, y en realidad lo encuentro más legible. Tenga en cuenta que si no le gusta este enfoque, simplemente use el método de constructor tradicional.

class UsersController { public function index(AllUsersQueryInterface $query) { // Fetch user data $users = $query->fetch([''first_name'', ''last_name'', ''email'']); // Return view return Response::view(''all_users.php'', [''users'' => $users]); } public function add() { return Response::view(''add_user.php''); } public function insert(UserRepositoryInterface $repository) { // Create new user model $user = new User; $user->first_name = $_POST[''first_name'']; $user->last_name = $_POST[''last_name'']; $user->gender = $_POST[''gender'']; $user->email = $_POST[''email'']; // Save the new user $repository->save($user); // Return the id return Response::json([''id'' => $user->id]); } public function view(SpecificUserQueryInterface $query, $id) { // Load user data if (!$user = $query->fetch($id, [''first_name'', ''last_name'', ''gender'', ''email''])) { return Response::notFound(); } // Return view return Response::view(''view_user.php'', [''user'' => $user]); } public function edit(SpecificUserQueryInterface $query, $id) { // Load user data if (!$user = $query->fetch($id, [''first_name'', ''last_name'', ''gender'', ''email''])) { return Response::notFound(); } // Return view return Response::view(''edit_user.php'', [''user'' => $user]); } public function update(UserRepositoryInterface $repository) { // Load user model if (!$user = $repository->find($id)) { return Response::notFound(); } // Update the user $user->first_name = $_POST[''first_name'']; $user->last_name = $_POST[''last_name'']; $user->gender = $_POST[''gender'']; $user->email = $_POST[''email'']; // Save the user $repository->save($user); // Return success return true; } public function delete(UserRepositoryInterface $repository) { // Load user model if (!$user = $repository->find($id)) { return Response::notFound(); } // Delete the user $repository->delete($user); // Return success return true; } }

Pensamientos finales:

Lo importante a tener en cuenta aquí es que cuando estoy modificando (creando, actualizando o eliminando) entidades, estoy trabajando con objetos de modelos reales y realizando la persistencia a través de mis repositorios.

Sin embargo, cuando estoy mostrando (seleccionando datos y enviándolos a las vistas) no estoy trabajando con objetos modelo, sino con objetos de valor antiguos. Solo selecciono los campos que necesito, y está diseñado para poder maximizar el rendimiento de mi búsqueda de datos.

Mis repositorios se mantienen muy limpios y, en cambio, este "desorden" se organiza en mis consultas modelo.

Utilizo un mapeador de datos para ayudar con el desarrollo, ya que es ridículo escribir SQL repetitivo para tareas comunes. Sin embargo, absolutamente puede escribir SQL donde sea necesario (consultas complicadas, informes, etc.). Y cuando lo haces, está bien escondido en una clase debidamente nombrada.

Me encantaría escuchar tu opinión sobre mi enfoque!

Actualización de julio de 2015:

Me han preguntado en los comentarios donde terminé con todo esto. Bueno, no tan lejos en realidad. A decir verdad, todavía no me gustan los repositorios. Los encuentro excesivos para búsquedas básicas (especialmente si ya está usando un ORM) y desordenado cuando se trabaja con consultas más complicadas.

Por lo general, trabajo con un ORM de estilo ActiveRecord, por lo que la mayoría de las veces solo haré referencia a esos modelos directamente en mi aplicación. Sin embargo, en situaciones donde tengo consultas más complejas, usaré objetos de consulta para hacerlos más reutilizables. También debo tener en cuenta que siempre inyecto mis modelos en mis métodos, lo que los hace más fáciles de burlar en mis pruebas.


Según mi experiencia, aquí hay algunas respuestas a sus preguntas:

P: ¿Cómo lidiamos con la devolución de campos que no necesitamos?

R: Desde mi experiencia, esto se reduce a tratar con entidades completas en lugar de consultas ad hoc.

Una entidad completa es algo así como un objeto User . Tiene propiedades y métodos, etc. Es un ciudadano de primera clase en su base de código.

Una consulta ad hoc devuelve algunos datos, pero no sabemos nada más allá de eso. A medida que los datos pasan a la aplicación, se hace sin contexto. ¿Es un User ? ¿Un User con alguna información de Order adjunta? Realmente no lo sabemos.

Prefiero trabajar con entidades completas.

Tiene razón en que a menudo devolverá datos que no usará, pero puede abordar esto de varias maneras:

  1. Agrupe en forma agresiva las entidades para que solo pague el precio de lectura una vez desde la base de datos.
  2. Dedica más tiempo a modelar tus entidades para que tengan buenas distinciones entre ellas. (Considere la posibilidad de dividir una entidad grande en dos entidades más pequeñas, etc.)
  3. Considere tener múltiples versiones de entidades. Puede tener un User para el back-end y tal vez un UserSmall para llamadas AJAX. Uno podría tener 10 propiedades y el otro tiene 3 propiedades.

Las desventajas de trabajar con consultas ad-hoc:

  1. Usted termina esencialmente con los mismos datos en muchas consultas. Por ejemplo, con un User , terminará escribiendo esencialmente la misma select * para muchas llamadas. Una llamada obtendrá 8 de 10 campos, otra obtendrá 5 de 10, otra obtendrá 7 de 10. ¿Por qué no reemplazar todas las llamadas con una llamada que obtiene 10 de 10? La razón por la que esto es malo es que es un asesinato re-factor / prueba / simulacro.
  2. Con el tiempo, se vuelve muy difícil razonar a un alto nivel sobre su código. En lugar de declaraciones como "¿Por qué el User tan lento?" se termina rastreando consultas únicas y, por lo tanto, las correcciones de errores tienden a ser pequeñas y localizadas.
  3. Es realmente difícil reemplazar la tecnología subyacente. Si almacena todo en MySQL ahora y desea mudarse a MongoDB, es mucho más difícil reemplazar 100 llamadas ad-hoc que un puñado de entidades.

P: Tendré demasiados métodos en mi repositorio.

R: Realmente no he visto otra forma de solucionar esto, aparte de consolidar llamadas. Las llamadas al método en su repositorio realmente se asignan a las características de su aplicación. Cuantas más características, más llamadas específicas de datos. Puedes presionar las funciones e intentar combinar llamadas similares en una sola.

La complejidad al final del día tiene que existir en algún lugar. Con un patrón de repositorio, lo hemos introducido en la interfaz del repositorio en lugar de hacer un montón de procedimientos almacenados.

A veces me tengo que decir: "¡Bueno, tenía que ceder en alguna parte! No hay balas de plata".


Solo puedo comentar sobre la forma en que nosotros (en mi empresa) tratamos esto. En primer lugar, el rendimiento no es un gran problema para nosotros, pero tener un código limpio / adecuado sí lo es.

En primer lugar, definimos modelos como UserModel que utiliza un ORM para crear objetos UserEntity . Cuando se carga un UserEntity desde un modelo, todos los campos se cargan. Para los campos que hacen referencia a entidades extranjeras, utilizamos el modelo externo apropiado para crear las entidades respectivas. Para esas entidades los datos se cargarán en cualquier momento. Ahora tu reacción inicial podría ser ... ??? ... !!! Déjame darte un ejemplo un poco de ejemplo:

class UserEntity extends PersistentEntity { public function getOrders() { $this->getField(''orders''); //OrderModel creates OrderEntities with only the ID''s set } } class UserModel { protected $orm; public function findUsers(IGetOptions $options = null) { return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities } } class OrderEntity extends PersistentEntity {} // user your imagination class OrderModel { public function findOrdersById(array $ids, IGetOptions $options = null) { //... } }

En nuestro caso, $db es un ORM que puede cargar entidades. El modelo le indica al ORM que cargue un conjunto de entidades de un tipo específico. El ORM contiene una asignación y la utiliza para inyectar todos los campos de esa entidad en la entidad. Sin embargo, para campos foráneos solo se cargan los ID de esos objetos. En este caso, OrderModel crea OrderEntity s solo con los ID de las órdenes a las que se hace referencia. Cuando se llama a PersistentEntity::getField por OrderEntity la entidad le indica a su modelo que cargue de forma perezosa todos los campos en OrderEntity s. Todas las OrderEntity s asociadas con una UserEntity se tratan como un conjunto de resultados y se cargarán a la vez.

La magia aquí es que nuestro modelo y ORM inyectan todos los datos en las entidades y que las entidades simplemente proporcionan funciones de envoltorio para el método getField genérico proporcionado por PersistentEntity Para resumir, siempre cargamos todos los campos, pero los campos que hacen referencia a una entidad extranjera se cargan cuando es necesario. Solo cargar un montón de campos no es realmente un problema de rendimiento. Cargar todas las entidades extranjeras posibles, sin embargo, sería una gran disminución de rendimiento.

Ahora, para cargar un conjunto específico de usuarios, basado en una cláusula where. Ofrecemos un paquete de clases orientado a objetos que le permite especificar expresiones simples que se pueden pegar entre sí. En el código de ejemplo lo nombré GetOptions . Es un contenedor para todas las opciones posibles para una consulta de selección. Contiene una colección de cláusulas where, una cláusula group by y todo lo demás. Nuestras cláusulas donde son bastante complicadas, pero obviamente podría hacer una versión más simple fácilmente.

$objOptions->getConditionHolder()->addConditionBind( new ConditionBind( new Condition(''orderProduct.product'', ICondition::OPERATOR_IS, $argObjProduct) ) );

Una versión más simple de este sistema sería pasar la parte DÓNDE de la consulta como una cadena directamente al modelo.

Lo siento por esta respuesta bastante complicada. Intenté resumir nuestro marco lo más rápido y claro posible. Si tiene alguna pregunta adicional, no dude en preguntarlas y actualizaré mi respuesta.

EDITAR: Además, si realmente no desea cargar algunos campos de inmediato, puede especificar una opción de carga diferida en su asignación ORM. Debido a que todos los campos se cargan finalmente a través del método getField , podría cargar algunos campos a última hora cuando se llame a ese método. Este no es un problema muy grande en PHP, pero no lo recomendaría para otros sistemas.



Yo uso las siguientes interfaces:

  • Repository : carga, inserta, actualiza y elimina entidades
  • Selector - encuentra entidades basadas en filtros, en un repositorio
  • Filter - encapsula la lógica de filtrado

Mi Repository es Repository base de datos; de hecho no especifica ninguna persistencia; podría ser cualquier cosa: base de datos SQL, archivo xml, servicio remoto, un extraterrestre del espacio exterior, etc. Para las capacidades de búsqueda, el Repository construye un Selector que se puede filtrar, LIMIT , ordenar y contar. Al final, el selector recupera una o más Entities de la persistencia.

Aquí hay un código de ejemplo:

<?php interface Repository { public function addEntity(Entity $entity); public function updateEntity(Entity $entity); public function removeEntity(Entity $entity); /** * @return Entity */ public function loadEntity($entityId); public function factoryEntitySelector():Selector } interface Selector extends /Countable { public function count(); /** * @return Entity[] */ public function fetchEntities(); /** * @return Entity */ public function fetchEntity(); public function limit(...$limit); public function filter(Filter $filter); public function orderBy($column, $ascending = true); public function removeFilter($filterName); } interface Filter { public function getFilterName(); }

Entonces, una implementación:

class SqlEntityRepository { ... public function factoryEntitySelector() { return new SqlSelector($this); } ... } class SqlSelector implements Selector { ... private function adaptFilter(Filter $filter):SqlQueryFilter { return (new SqlSelectorFilterAdapter())->adaptFilter($filter); } ... } class SqlSelectorFilterAdapter { public function adaptFilter(Filter $filter):SqlQueryFilter { $concreteClass = (new StringRebaser( ''Filter//', ''SqlQueryFilter//')) ->rebase(get_class($filter)); return new $concreteClass($filter); } }

La idea es que el Selector genérico usa Filter pero la implementación SqlSelector usa SqlFilter ; SqlSelectorFilterAdapter adapta un Filter genérico a un SqlFilter concreto.

El código del cliente crea objetos de Filter (que son filtros genéricos), pero en la implementación concreta del selector esos filtros se transforman en filtros de SQL.

Otras implementaciones de selector, como InMemorySelector , se transforman de Filter a InMemoryFilter usando su InMemorySelectorFilterAdapter específico; Entonces, cada implementación de selector viene con su propio adaptador de filtro.

Al usar esta estrategia, a mi código de cliente (en la capa de negocios) no le importa un repositorio específico o una implementación de selector.

/** @var Repository $repository*/ $selector = $repository->factoryEntitySelector(); $selector->filter(new AttributeEquals(''activated'', 1))->limit(2)->orderBy(''username''); $activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit() $activatedUsers = $selector->fetchEntities();

PS Esta es una simplificación de mi código real.