php - preflush - Doctrine Entities y lógica de negocios en una aplicación Symfony
symfony doctrine preflush (5)
Como preferencia personal, me gusta comenzar de manera simple y crecer a medida que se aplican más reglas comerciales. Como tal, tiendo a favorecer que los oyentes se acerquen mejor .
Usted acaba de
- agregue más oyentes a medida que las reglas de negocios evolucionan ,
- cada uno tiene una sola responsabilidad ,
- y puedes probar estos oyentes de forma independiente más fácil.
Algo que requeriría muchos simulacros / talones si tiene una única clase de servicio, como:
class SomeService
{
function someMethod($argA, $argB)
{
// some logic A.
...
// some logic B.
...
// feature you want to test.
...
// some logic C.
...
}
}
Cualquier idea / comentario es bienvenido :)
Me encuentro con un problema sobre cómo manejar la lógica empresarial en torno a mis entidades Doctrine2 en una gran aplicación Symfony2 . (Perdón por la duración de la publicación)
Después de leer muchos blogs, libros de cocina y otros recursos, encuentro que:
- Las entidades solo se pueden usar para la persistencia del mapeo de datos ("modelo anémico"),
- Los controladores deben ser lo más delgado posible,
- Los modelos de dominio deben estar desacoplados de la capa de persistencia (entidad no conoce el administrador de la entidad)
Ok, estoy totalmente de acuerdo con eso, pero: ¿ dónde y cómo manejar reglas de negocios complejas en modelos de dominio?
Un simple ejemplo
NUESTROS MODELOS DE DOMINIO:
- un grupo puede usar Roles
- un rol puede ser utilizado por diferentes grupos
- un usuario puede pertenecer a muchos grupos con muchos roles ,
En una capa de persistencia de SQL , podríamos modelar estas relaciones como:
NUESTRAS REGLAS DE NEGOCIO ESPECÍFICAS:
- El usuario puede tener Roles en Grupos solo si Roles está unido al Grupo .
- Si separamos un Rol R1 de un Grupo G1 , todas las UserRoleAffectation con el Grupo G1 y el Rol R1 deben borrarse
Este es un ejemplo muy simple, pero me gustaría saber cuál es la mejor manera de administrar estas reglas comerciales.
Soluciones encontradas
1- Implementación en la capa de servicio
Use una clase de servicio específica como:
class GroupRoleAffectionService {
function linkRoleToGroup ($role, $group)
{
//...
}
function unlinkRoleToGroup ($role, $group)
{
//business logic to find all invalid UserRoleAffectation with these role and group
...
// BL to remove all found UserRoleAffectation OR to throw exception.
...
// detach role
$group->removeRole($role)
//save all handled entities;
$em->flush();
}
- (+) un servicio por clase / por regla de negocio
- (-) Las entidades API no representan al dominio: es posible llamar a
$group->removeRole($role)
desde este servicio. - (-) Demasiadas clases de servicio en una gran aplicación?
2 - Implementación en Administradores de entidades de dominio
Encapsule estas Lógica empresarial en el "administrador de entidades de dominio" específico, también llame a los Proveedores del modelo:
class GroupManager {
function create($name){...}
function remove($group) {...}
function store($group){...}
// ...
function linkRole($group, $role) {...}
function unlinkRoleToGroup ($group, $role)
{
// ... (as in previous service code)
}
function otherBusinessRule($params) {...}
}
- (+) todas las reglas de negocio están centralizadas
- (-) Las entidades API no representan el dominio: es posible llamar al $ group-> removeRole ($ role) fuera del servicio ...
- (-) ¿Administradores de dominio se convierte en administradores de FAT?
3 - Use oyentes cuando sea posible
Use los oyentes de eventos de Symfony y / o Doctrine:
class CheckUserRoleAffectationEventSubscriber implements EventSubscriber
{
// listen when a M2M relation between Group and Role is removed
public function getSubscribedEvents()
{
return array(
''preRemove''
);
}
public function preRemove(LifecycleEventArgs $event)
{
// BL here ...
}
4 - Implementar modelos enriquecidos extendiendo entidades
Utilice Entidades como clase secundaria / parental de clases de Modelos de dominio, que encapsulan gran cantidad de lógica de dominio. Pero esta solución parece más confusa para mí.
Para usted, ¿cuál es la mejor manera de administrar esta lógica comercial, centrándose en el código más limpio, desacoplado y comprobable? Sus comentarios y buenas prácticas? ¿Tienes ejemplos concretos?
Recursos principales:
- Entidades administradoras de Symfony
- Symfony2 / Doctrine, teniendo que poner lógica de negocio en mi controlador? Y duplicar el controlador?
- Extensión de Doctrine Entity para agregar lógica de negocio
- http://iamproblematic.com/2012/03/12/putting-your-symfony2-controllers-on-a-diet-part-2/
- http://l3l0.eu/lang/en/2012/04/anemic-domain-model-problem-in-symfony2/
- https://leanpub.com/a-year-with-symfony
Consideraría usar una capa de servicio aparte de las entidades en sí. Las clases de entidades deberían describir las estructuras de datos y eventualmente algunos otros cálculos simples. Las reglas complejas van a los servicios.
Mientras use los servicios, puede crear más sistemas desacoplados, servicios, etc. Puede aprovechar la ventaja de la inyección de dependencia y utilizar eventos (despachadores y oyentes) para hacer la comunicación entre los servicios y mantenerlos débilmente conectados.
Lo digo sobre la base de mi propia experiencia. Al principio solía poner toda la lógica dentro de las clases de entidades (especialmente cuando desarrollé aplicaciones Symfony 1.x / doctrine 1.x). Mientras crecieron las aplicaciones, se hicieron realmente difíciles de mantener.
Encuentro que la solución 1) es la más fácil de mantener desde una perspectiva más larga. La Solución 2 lidera la clase hinchada "Manager" que eventualmente se dividirá en trozos más pequeños.
http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData
"Demasiadas clases de servicio en una aplicación grande" no es motivo para evitar SRP.
En términos de lenguaje de dominio, encuentro el siguiente código similar:
$groupRoleService->removeRoleFromGroup($role, $group);
y
$group->removeRole($role);
También a partir de lo que describió, la eliminación / adición de roles del grupo requiere muchas dependencias (principio de inversión de dependencia) y eso podría ser difícil con un administrador FAT / hinchado.
Solución 3) se ve muy similar a 1): cada suscriptor es en realidad un servicio desencadenado automáticamente en segundo plano por Entity Manager y en escenarios más simples puede funcionar, pero surgirán problemas tan pronto como la acción (agregar / quitar rol) requerirá mucho contexto p.ej. qué usuario realizó la acción, desde qué página o cualquier otro tipo de validación compleja.
Estoy a favor de las entidades con conciencia empresarial . Doctrine recorre un largo camino para no contaminar su modelo con problemas de infraestructura; usa reflexión para que pueda modificar los accesos como desee. Las 2 cosas de "Doctrine" que pueden permanecer en las clases de su entidad son anotaciones (puede evitarlas gracias a la asignación de YML) y ArrayCollection
. Esta es una biblioteca fuera de Doctrine ORM ( Doctrine/Common
), por lo que no hay problemas allí.
Entonces, apegándonos a los principios básicos de DDD, las entidades son realmente el lugar para poner su lógica de dominio. Por supuesto, a veces esto no es suficiente, entonces usted es libre de agregar servicios de dominio , servicios sin problemas de infraestructura.
Los repositorios de Doctrine están más en el medio: prefiero mantenerlos como la única forma de consultar entidades, eventos si no se apegan al patrón de repositorio inicial y prefiero eliminar los métodos generados. Agregar el servicio del administrador para encapsular todas las operaciones de búsqueda / guardado de una clase determinada fue una práctica común de Symfony hace algunos años, no me gusta mucho.
En mi experiencia, es posible que tengas muchos más problemas con el componente de formulario Symfony, no sé si lo usas. Limitarán seriamente su capacidad de personalizar el constructor, entonces puede usar constructores con nombre. Agregar PhpDoc @deprecated̀
tag le dará a sus pares algunos comentarios visuales que no deberían demandar al constructor original.
Por último, depender demasiado de los eventos de Doctrine te morderá. Aquí hay demasiadas limitaciones técnicas, y encuentro difíciles de seguir. Cuando sea necesario, agrego eventos de dominio enviados desde el controlador / comando al distribuidor de eventos Symfony.
Ver aquí: Sf2: usar un servicio dentro de una entidad
Tal vez mi respuesta aquí ayude. Simplemente trata eso: cómo "desacoplar" el modelo frente a la persistencia frente a las capas de controlador.
En su pregunta específica, diría que hay un "truco" aquí ... ¿qué es un "grupo"? Es "solo"? o cuando se relaciona con alguien?
Inicialmente sus clases de modelo probablemente podrían verse así:
UserManager (service, entry point for all others)
Users
User
Groups
Group
Roles
Role
UserManager tendría métodos para obtener los objetos del modelo (como se dice en esa respuesta, nunca debe hacer uno new
). En un controlador, puedes hacer esto:
$userManager = $this->get( ''myproject.user.manager'' );
$user = $userManager->getUserById( 33 );
$user->whatever();
Entonces ... El User
, como usted dice, puede tener roles, que pueden asignarse o no.
// Using metalanguage similar to C++ to show return datatypes.
User
{
// Role managing
Roles getAllRolesTheUserHasInAnyGroup();
void addRoleById( Id $roleId, Id $groupId );
void removeRoleById( Id $roleId );
// Group managing
Groups getGroups();
void addGroupById( Id $groupId );
void removeGroupById( Id $groupId );
}
He simplificado, por supuesto, puede agregar por Id, agregar por Objeto, etc.
Pero cuando piensas esto en "lenguaje natural" ... veamos ...
- Sé que Alice pertenece a un fotógrafo.
- Obtengo el objeto Alice.
- Pregunto a Alice sobre los grupos. Obtengo el grupo Fotógrafos.
- Pregunto a los fotógrafos sobre los roles.
Ver más en detalle:
- Sé que Alice es id. De usuario = 33 y está en el grupo de fotógrafos.
- Solicito a Alice al UserManager a través de
$user = $manager->getUserById( 33 );
- Accedo al grupo Fotógrafos a través de Alicia, tal vez con `$ group = $ user-> getGroupByName (''Photographers'');
- Entonces me gustaría ver los roles del grupo ... ¿Qué debería hacer?
- Opción 1: $ group-> getRoles ();
- Opción 2: $ group-> getRolesForUser ($ userId);
El segundo es redundante, ya que conseguí el grupo a través de Alicia. Puede crear una nueva clase GroupSpecificToUser
que herede de Group
.
Similar a un juego ... ¿qué es un juego? El "juego" como el "ajedrez" en general? ¿O el "juego" específico de "ajedrez" que tú y yo comenzamos ayer?
En este caso $user->getGroups()
devolvería una colección de objetos GroupSpecificToUser.
GroupSpecificToUser extends Group
{
User getPointOfViewUser()
Roles getRoles()
}
Este segundo enfoque le permitirá encapsular allí muchas otras cosas que aparecerán tarde o temprano: ¿Se permite a este usuario hacer algo aquí? solo puede consultar la subclase del grupo: $group->allowedToPost();
, $group->allowedToChangeName();
, $group->allowedToUploadImage();
, etc.
En cualquier caso, puede evitar crear una clase extraña y simplemente preguntarle al usuario sobre esta información, como un $user->getRolesForGroup( $groupId );
enfoque.
El modelo no es capa de persistencia
Me gusta ''olvidarme'' de la tolerancia al diseñar. Normalmente me siento con mi equipo (o conmigo mismo, para proyectos personales) y paso 4 o 6 horas pensando antes de escribir cualquier línea de código. Escribimos una API en un documento txt. A continuación, repítelo agregando, eliminando métodos, etc.
Una posible API de "punto de partida" para su ejemplo podría contener consultas de cualquier cosa, como un triángulo:
User
getId()
getName()
getAllGroups() // Returns all the groups to which the user belongs.
getAllRoles() // Returns the list of roles the user has in any possible group.
getRolesOfACertainGroup( $group ) // Returns the list of groups for which the user has that specific role.
getGroupsOfRole( $role ) // Returns all the roles the user has in a specific group.
addRoleToGroup( $group, $role )
removeRoleFromGroup( $group, $role )
removeFromGroup() // Probably you want to remove the user from a group without having to loop over all the roles.
// removeRole() ?? // Maybe you want (or not) remove all admin privileges to this user, no care of what groups.
Group
getId()
getName()
getAllUsers()
getAllRoles()
getAllUsersWithRole( $role )
getAllRolesOfUser( $user )
addUserWithRole( $user, $role )
removeUserWithRole( $user, $role )
removeUser( $user ) // Probably you want to be able to remove a user completely instead of doing it role by role.
// removeRole( $role ) ?? // Probably you don''t want to be able to remove all the roles at a time (say, remove all admins, and leave the group without any admin)
Roles
getId()
getName()
getAllUsers() // All users that have this role in one or another group.
getAllGroups() // All groups for which any user has this role.
getAllUsersForGroup( $group ) // All users that have this role in the given group.
getAllGroupsForUser( $user ) // All groups for which the given user is granted that role
// Querying redundantly is natural, but maybe "adding this user to this group"
// from the role object is a bit weird, and we already have the add group
// to the user and its redundant add user to group.
// Adding it to here maybe is too much.
Eventos
Como dije en el artículo puntiagudo, también lanzaría eventos en el modelo,
Por ejemplo, al eliminar un rol de un usuario en un grupo, pude detectar en un "oyente" que si ese era el último administrador, puedo a) cancelar la eliminación del rol, b) permitirlo y abandonar el grupo sin administrador, c) permitirlo pero elija un nuevo administrador con los usuarios en el grupo, etc. o la política que sea adecuada para usted.
De la misma manera, tal vez un usuario solo puede pertenecer a 50 grupos (como en LinkedIn). A continuación, puede lanzar un evento preAddUserToGroup y cualquier receptor podría contener el conjunto de reglas de prohibición cuando el usuario desea unirse al grupo 51.
Esa "regla" puede dejar claramente fuera de la clase Usuario, Grupo y Rol y salir en una clase de nivel superior que contiene las "reglas" mediante las cuales los usuarios pueden unirse o abandonar grupos.
Recomiendo encarecidamente ver la otra respuesta.
Espero ayudar!
Xavi.