Search by

develia / symfony

antonio-gil-espinosa

Symfony utility library

0.7.2 2026-11-04 01:12 UTC

README

Este paquete proporciona una serie de utilidades y servicios para acelerar el desarrollo de aplicaciones Symfony en el ecosistema Develia.

Características principales

  • Controladores Base:

    • ApiController: Clase base para controladores de API. deserialize() y populate() leen del Request el cuerpo JSON o, si la petición es un formulario (multipart o URL-encoded), sus campos convertidos a los tipos declarados del objeto (bool, int, float, "" a null), en Symfony 7.4 y 8; un campo cuya propiedad es un objeto o un array se decodifica si llega como texto JSON, como envían los objetos los clientes generados. Los ficheros subidos se leen aparte.
    • ExceptionStatusMapperInterface: decide el código HTTP de las respuestas de error de los ApiController. La implementación por defecto, StandardExceptionStatusMapper, responde 4xx a las excepciones HTTP de Symfony, 400 a InvalidArgumentException, DomainException, OutOfRangeException y OutOfBoundsException, y 500 al resto (sin mostrar el mensaje). Las excepciones a las que asigna un 4xx se registran en el log como errores de cliente (ERROR) y no como fallos críticos. La aplicación asigna sus excepciones decorando el servicio, sin que el dominio dependa de HTTP:

      #[AsDecorator(ExceptionStatusMapperInterface::class)]
      final class AppExceptionStatusMapper implements ExceptionStatusMapperInterface
      {
          public function __construct(
              #[AutowireDecorated] private readonly ExceptionStatusMapperInterface $inner,
          ) {}
      
          public function getStatusCode(Throwable $exception): int
          {
              return match (true) {
                  $exception instanceof InvoiceAlreadyPaidException => Response::HTTP_CONFLICT,
                  default => $this->inner->getStatusCode($exception),
              };
          }
      }
      

      En versiones de Symfony sin esos atributos, se declara con decorates en services.yaml.

    • RestController: Implementación básica para controladores REST con métodos GET, POST, PUT y DELETE mapeados automáticamente.
    • BaseCrudController: Facilitador para operaciones CRUD estándar.
  • Servicios de Gestión:

    • SettingsManager: Interfaz y servicio para gestionar configuraciones dinámicas (con soporte para base de datos y caché); permite leer, guardar y eliminar un ajuste o todos los de un ámbito.
    • DatabaseLogger: Logger compatible con PSR-3 que persiste los registros en la base de datos (tablas log_entry y log_entry_meta). LogEntryNormalizer serializa sus entradas (id, level, message, createdAt, scope, scopeId y los metadatos como context), y develia:logs:purge --days=90 (LogEntryRepository::purgeOlderThan()) borra las más antiguas con sus metadatos; la aplicación lo programa, p. ej. a diario en su Schedule.
    • OpenAiService: Integración simplificada con la API de OpenAI.
    • JsonSchemaGenerator: Genera JSON Schema a partir de clases PHP (para Structured Outputs de OpenAI).
    • YouTrackService: Cliente de la API REST de YouTrack (incidencias, comentarios, usuarios y roles). Configurable con las variables de entorno YOUTRACK_BASE_URL, YOUTRACK_TOKEN y YOUTRACK_PROJECT_ID. Al crear usuarios la contraseña es obligatoria (YouTrack la exige) y, en YouTrack Cloud, deleteUser() no está disponible: usar banUser(). getIssues() y getUsers() aceptan una consulta opcional para filtrar en el servidor (reporter: <login>, login o nombre del usuario…). Los roles se gestionan en Hub (getRoles(), findRole(), getUserRoles(), assignUserRole()), que identifica a los usuarios por su ringId y sólo permite conceder roles sobre proyectos registrados en Hub; para el resto, usar el proyecto global (HUB_GLOBAL_PROJECT_ID, valor por defecto).
    • SlackNotifier: Envía notificaciones a Slack mediante un Incoming Webhook. Configurable con la variable de entorno SLACK_WEBHOOK_URL.
    • TransactionManager: Utilidad para envolver ejecuciones en transacciones de base de datos.
    • LockManager: Gestión de bloqueos (locks) para evitar condiciones de carrera.
  • Listados filtrados en servidor:

    • Listing\FilterSet: Declaración inmutable, creada con FilterSet::builder() (FilterSetBuilder: addFilter, addSortableColumn, setDefaultSort, setTieBreaker, setPageSize y build()), de los filtros, columnas ordenables y límites de paginación de un listado. resolve() convierte los parámetros de la petición (page, itemsPerPage, sortBy, sortDesc y un parámetro por filtro, como los que envía useServerTable de @develia/vuetify) en un ListingCriteria listo para BaseRepository::findAllBySpecs(). Ignora los filtros vacíos, rechaza con InvalidArgumentException las columnas no ordenables, limita itemsPerPage y añade un campo de desempate para que las páginas sean estables.
  • Seguridad:

    • StaticApiKeyAuthenticator: Autenticador sencillo mediante una clave API estática enviada en las cabeceras.
    • ApiKeyAuthenticator y BearerApiKeyAuthenticator: Autenticadores opcionales e independientes que localizan un usuario mediante un cargador configurable usando X-Api-Key o Authorization: Bearer.
    • OAuthAuthenticator: inicio de sesión con los clientes de knpuniversity/oauth2-client-bundle (Google, Facebook...), con rutas propias y la decisión de qué usuario entra en manos de la aplicación.

Inicio de sesión con OAuth

Se registra cuando están instalados knpuniversity/oauth2-client-bundle y los componentes de seguridad. La librería hace el flujo: la redirección al proveedor, la comprobación del state guardado en la sesión, el intercambio del código y los errores. La aplicación decide qué usuario entra con cada identidad implementando OAuthUserResolverInterface: lo busca, lo vincula a una cuenta existente o lo crea, o rechaza el acceso con CustomUserMessageAuthenticationException, cuyo mensaje se muestra en el login. Vincular por email solo es seguro si el proveedor garantiza que el email es del usuario (p. ej. email_verified de Google).

# config/routes/develia_oauth.yaml: /oauth/{client}/connect (inicio) y /oauth/{client}/check (vuelta)
develia_oauth:
    resource: '@DeveliaBundle/Resources/config/routes/oauth.php'

# config/packages/knpu_oauth2_client.yaml: cada cliente vuelve a develia_oauth_check con su nombre
knpu_oauth2_client:
    clients:
        google:
            type: google
            client_id: '%env(OAUTH_GOOGLE_CLIENT_ID)%'
            client_secret: '%env(OAUTH_GOOGLE_CLIENT_SECRET)%'
            redirect_route: develia_oauth_check
            redirect_params: { client: google }

# config/services.yaml
parameters:
    # Clientes con los que se puede iniciar sesión y sus permisos; los demás responden 404.
    develia.security.oauth_scopes: { google: [openid, email, profile] }
    develia.security.oauth_login_route: app_login           # adonde vuelve tras un error (por defecto, app_login)
    develia.security.oauth_target_route: app_login_success  # tras entrar, si ninguna página pidió la sesión (por defecto, /)
services:
    Develia\Symfony\Security\OAuth\OAuthUserResolverInterface: '@App\Security\OAuthUserResolver'

# config/packages/security.yaml
security:
    firewalls:
        main:
            custom_authenticators:
                - Develia\Symfony\Security\OAuth\OAuthAuthenticator

Como form_login, tras entrar se vuelve a la página que pidió la sesión; tras un error, al login con el error en la sesión (AuthenticationUtils::getLastAuthenticationError()), también si el proveedor no responde. El user_checker del firewall se aplica igual que en cualquier otro inicio de sesión, y con remember_me y always_remember_me la sesión también se recuerda.

  • Hace falta el bundle de knpu activado, no solo instalado.
  • Los clientes deben conservar use_state (activado por defecto): el state de la sesión es lo que impide que un tercero inicie la sesión de otro con su propia cuenta (CSRF del login).
  • Un cliente con ~ en develia.security.oauth_scopes queda desactivado, igual que uno que no aparece: sus rutas responden 404.
  • La vuelta solo admite GET; no sirven los proveedores que la envían por POST (form_post).

Autenticación API/MCP

La librería no crea rutas, servidores MCP, firewalls ni reglas de autorización. La aplicación consumidora puede preparar su entidad Doctrine con ApiKeyUserTrait, sin implementar un contrato adicional al UserInterface requerido por Symfony:

StaticApiKeyAuthenticator valida una única clave estática y delega la carga de api_user al proveedor configurado en el firewall, que es quien determina sus roles. El cuarto argumento del constructor permite usar otro identificador sin extender el autenticador:

services:
    Develia\Symfony\Security\StaticApiKeyAuthenticator:
        arguments:
            $apiKey: '%env(STATIC_API_KEY)%'
            $userIdentifier: service_account
use Develia\Symfony\Security\ApiKeyUserTrait;

class User implements UserInterface
{
    use ApiKeyUserTrait;

    // Implementar aquí el resto de UserInterface.
}

El trait añade una columna Doctrine nullable, única y de hasta 255 caracteres. La aplicación debe generar la migración y no debe incluir apiKey en sus grupos de Serializer ni exponerlo en respuestas.

Por defecto se usa PlainTextApiKeyEncoder. Para almacenar el valor de búsqueda como SHA-256 determinista, sustituye el alias en la aplicación:

services:
    Develia\Symfony\Security\ApiKeyEncoderInterface:
        alias: Develia\Symfony\Security\Sha256ApiKeyEncoder

El mismo encoder debe usarse al crear o rotar la clave y al autenticarla. SHA-256 determinista permite una consulta directa por apiKey y es apropiado para claves aleatorias largas; no sustituye un hash adaptativo para secretos de baja entropía.

$rawApiKey = bin2hex(random_bytes(32));
$user->setApiKey($apiKeyEncoder->encode($rawApiKey));

// Entregar $rawApiKey una sola vez al propietario y no registrarla.

Por defecto, DoctrineApiKeyUserLoader busca el valor codificado en apiKey. Solo hay que indicar la entidad de usuario; la propiedad también puede personalizarse:

parameters:
    develia.security.api_key_user_class: App\Entity\User
    develia.security.api_key_property: apiKey

Para usar Redis, una API remota u otro almacenamiento, implementa ApiKeyUserLoaderInterface y sustituye su alias. La aplicación conecta los autenticadores a sus firewalls API o MCP y sigue siendo responsable de las rutas y la autorización:

services:
    Develia\Symfony\Security\ApiKeyUserLoaderInterface:
        alias: App\Security\ApiKeyUserLoader
security:
    firewalls:
        api:
            stateless: true
            custom_authenticators:
                - Develia\Symfony\Security\ApiKeyAuthenticator
                - Develia\Symfony\Security\BearerApiKeyAuthenticator

ApiKeyAuthenticator usa X-Api-Key por defecto; el parámetro de servicio develia.security.api_key_header permite cambiarlo sin configurar BearerApiKeyAuthenticator. Los dos autenticadores no inspeccionan el transporte del otro ni rechazan su presencia simultánea. Los fallos devuelven 401 sin la credencial y el flujo Bearer añade WWW-Authenticate: Bearer.

  • Extensiones Twig:

    • DateExtension: Filtros adicionales para el manejo de fechas en plantillas.
  • Utilidades:

    • Helpers para manejo de anotaciones, rutas y respuestas XML.
    • CommandEmitter implementa Develia\TaskOutputInterface y serializa cada TaskEvent en la salida de Symfony Console. sendPayload() se mantiene para consumidores anteriores que todavía envían otros objetos.

Requisitos

  • PHP 8.1 o superior.
  • develia/commons, FrameworkBundle, Serializer, Console, Process, Lock, HttpClient y YAML se instalan como dependencias del paquete.
  • Los servicios de IA (OpenAIService, AnthropicAIService, NameGenderClassifier y FullNameSplitter) solo se registran con openai-php/client instalado, que necesita una implementación de PSR-17, como nyholm/psr7: composer require openai-php/client nyholm/psr7.
  • Los componentes Cache, Config, DependencyInjection, EventDispatcher, HttpFoundation, HttpKernel y Routing los aporta FrameworkBundle.

Instalación

Instala el paquete vía Composer:

composer require develia/symfony

La instalación base no incluye Doctrine. Para habilitar BaseRepository, entidades y specifications, mappings ORM, DatabaseLogger, DatabaseSettingsManager, TransactionManager, PdoLockManager, LogEntryRepository y el loader Doctrine de API keys, instala:

composer require doctrine/doctrine-bundle doctrine/orm

El bundle detecta DoctrineBundle, ORM y DBAL y registra automáticamente sus mappings, servicios y aliases. Aunque symfony/lock forma parte de la instalación base, la implementación PdoLockManager solo se registra cuando DBAL está disponible.

Con Doctrine disponible, el bundle también registra el tipo DBAL time_of_day (TimeOfDayType), compatible con DBAL 3 y 4, que persiste un Develia\TimeOfDay en una columna TIME:

#[ORM\Column(type: TimeOfDayType::NAME, nullable: true)]
private ?TimeOfDay $scheduledTime = null;

La autenticación es opcional y requiere los componentes de seguridad. El loader y los autenticadores de usuario se registran automáticamente cuando también está disponible Doctrine; sin Doctrine, la aplicación puede implementar ApiKeyUserLoaderInterface y registrar manualmente el loader y los autenticadores.

composer require symfony/security-core symfony/security-http

Para habilitar DateExtension, instala Twig:

composer require twig/twig

AnnotationUtilities, los formularios y JsonSchemaGenerator también mantienen sus dependencias opcionales indicadas en las sugerencias de Composer.

Asegúrate de registrar el bundle en tu archivo config/bundles.php si no se hace automáticamente:

return [
    // ...
    Develia\Symfony\DeveliaBundle::class => ['all' => true],
];

Actualizar a 0.7

La tabla settings guarda los ajustes sin identificador de ámbito (los globales) con scope_id vacío en vez de NULL y tiene un índice único sobre scope, scope_id y name, que antes dejaba entrar filas repetidas. Antes de generar la migración con doctrine:migrations:diff, añade al principio de su up() el borrado de las filas repetidas, que conserva la más reciente, y el cambio de NULL a vacío:

// PostgreSQL
$this->addSql("DELETE FROM settings a USING settings b WHERE a.scope = b.scope AND COALESCE(a.scope_id, '') = COALESCE(b.scope_id, '') AND a.name = b.name AND a.id < b.id");
// MariaDB / MySQL
$this->addSql("DELETE a FROM settings a JOIN settings b ON a.scope = b.scope AND COALESCE(a.scope_id, '') = COALESCE(b.scope_id, '') AND a.name = b.name AND a.id < b.id");

$this->addSql("UPDATE settings SET scope_id = '' WHERE scope_id IS NULL");

SettingsManagerInterface no cambia: null sigue significando «sin identificador».

Configuración

Puedes configurar los servicios en tus archivos de configuración de Symfony (ej. config/packages/develia.yaml). Consulta los servicios disponibles en el contenedor de servicios de Symfony.

Licencia

Propiedad de Antonio Gil Espinosa. Todos los derechos reservados.