PHPackages                             wappcode/pdss-utilities - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. wappcode/pdss-utilities

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

wappcode/pdss-utilities
=======================

Utilidades para PHP Doctrine Server Side

4.0.1(2mo ago)03162MITPHPPHP &gt;=8.0CI failing

Since Jun 29Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/wappcode/pdss-utilities)[ Packagist](https://packagist.org/packages/wappcode/pdss-utilities)[ RSS](/packages/wappcode-pdss-utilities/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (16)Versions (16)Used By (2)

PDSS-Utilities
==============

[](#pdss-utilities)

Utilidades PHP para trabajar con Doctrine ORM.

Requisitos
----------

[](#requisitos)

- PHP &gt;= 8.0
- Doctrine ORM &gt;= 3.0

Instalación
-----------

[](#instalación)

Instalar la librería usando Composer:

```
composer require wappcode/pdss-utilities
```

### Instalar dependencia de Doctrine ORM

[](#instalar-dependencia-de-doctrine-orm)

Si aún no tienes Doctrine ORM instalado:

```
composer require doctrine/orm
```

---

Clases Base para Entidades
--------------------------

[](#clases-base-para-entidades)

Clases abstractas que proveen gestión automática de timestamps (`created` y `updated`) y diferentes tipos de identificadores.

### AbstractEntityModel

[](#abstractentitymodel)

Clase base para entidades con ID tipo **integer auto-generado**.

```
use PDSSUtilities\AbstractEntityModel;

#[ORM\Entity]
class Product extends AbstractEntityModel
{
    #[ORM\Column(type: 'string')]
    private string $name;

    // Los campos id, created y updated están heredados
}
```

**Propiedades heredadas:**

- `id`: `?int` - Auto-generado por Doctrine
- `created`: `DateTimeImmutable` - Timestamp de creación
- `updated`: `DateTimeImmutable` - Timestamp de última actualización

### AbstractEntityModelUlid

[](#abstractentitymodelulid)

Clase base para entidades con ID tipo **ULID** (26 caracteres).

```
use PDSSUtilities\AbstractEntityModelUlid;

#[ORM\Entity]
class User extends AbstractEntityModelUlid
{
    #[ORM\Column(type: 'string')]
    private string $email;
}
```

**Propiedades heredadas:**

- `id`: `?string` - ULID de 26 caracteres (sortable por timestamp)
- `created`: `DateTimeImmutable`
- `updated`: `DateTimeImmutable`

### AbstractEntityModelKsuid

[](#abstractentitymodelksuid)

Clase base para entidades con ID tipo **KSUID** (27 caracteres).

```
use PDSSUtilities\AbstractEntityModelKsuid;

#[ORM\Entity]
class Order extends AbstractEntityModelKsuid
{
    #[ORM\Column(type: 'decimal')]
    private float $total;
}
```

**Propiedades heredadas:**

- `id`: `?string` - KSUID de 27 caracteres (K-Sortable Unique Identifier)
- `created`: `DateTimeImmutable`
- `updated`: `DateTimeImmutable`

### AbstractEntityModelUuidV4

[](#abstractentitymodeluuidv4)

Clase base para entidades con ID tipo **UUID v4** (36 caracteres).

```
use PDSSUtilities\AbstractEntityModelUuidV4;

#[ORM\Entity]
class Invoice extends AbstractEntityModelUuidV4
{
    #[ORM\Column(type: 'string')]
    private string $number;
}
```

**Propiedades heredadas:**

- `id`: `?string` - UUID v4 de 36 caracteres (formato: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
- `created`: `DateTimeImmutable`
- `updated`: `DateTimeImmutable`

### Métodos Disponibles

[](#métodos-disponibles)

Todas las clases abstract proveen:

```
public function getId(): ?string|?int;
public function getCreated(): DateTimeImmutable;
public function getUpdated(): DateTimeImmutable;
public function __toString(): string;
protected function setUpdated(): self; // Para actualización manual
```

---

¿Qué tipo de ID elegir?
-----------------------

[](#qué-tipo-de-id-elegir)

### AbstractEntityModel (Integer)

[](#abstractentitymodel-integer)

**Cuándo usar:**

- Tablas pequeñas a medianas (&lt; 2 mil millones de registros)
- Sistemas legacy que requieren IDs numéricos
- Cuando el orden de inserción es importante y secuencial
- APIs públicas donde prefieres IDs cortos y simples
- Cuando necesitas operaciones matemáticas con IDs

**Ventajas:**

- ✅ Tamaño mínimo (4-8 bytes)
- ✅ Índices más rápidos y compactos
- ✅ Fácil de leer y debuggear
- ✅ Compatible con sistemas antiguos

**Desventajas:**

- ❌ Predecible (riesgo de seguridad en APIs públicas)
- ❌ Revela cantidad de registros
- ❌ Problemas en sistemas distribuidos
- ❌ Límite de ~4 mil millones (INT) o ~9 quintillones (BIGINT)

### AbstractEntityModelUlid (26 caracteres)

[](#abstractentitymodelulid-26-caracteres)

**Cuándo usar:**

- **Recomendado para la mayoría de casos**
- Sistemas modernos que requieren IDs únicos globalmente
- Cuando necesitas ordenamiento cronológico natural
- Microservicios y arquitecturas distribuidas
- Migración desde sistemas con auto-increment

**Ventajas:**

- ✅ Sortable por tiempo de creación
- ✅ Único globalmente sin coordinación
- ✅ Más compacto que UUID (26 vs 36 caracteres)
- ✅ Case-insensitive (Base32)
- ✅ Legible (no usa caracteres ambiguos)
- ✅ Performance excelente

**Desventajas:**

- ❌ Más grande que integers (26 bytes vs 4-8)
- ❌ No es estándar universal como UUID

**Ideal para:** Users, Orders, Products, Posts, Comments

### AbstractEntityModelKsuid (27 caracteres)

[](#abstractentitymodelksuid-27-caracteres)

**Cuándo usar:**

- Similar a ULID pero con mayor precisión temporal
- Sistemas que requieren ordenamiento muy preciso
- Cuando necesitas timestamp en epoch específico (2014)
- Requerimiento específico de formato KSUID

**Ventajas:**

- ✅ Sortable por tiempo (epoch 2014)
- ✅ Único globalmente
- ✅ Buena distribución en índices

**Desventajas:**

- ❌ Case-sensitive (Base62: A-Z, a-z, 0-9)
- ❌ Menos común que ULID o UUID
- ❌ Requiere extensión GMP
- ❌ 27 caracteres (el más largo)

**Ideal para:** Logs, Events, Audit trails, Time-series data

### AbstractEntityModelUuidV4 (36 caracteres)

[](#abstractentitymodeluuidv4-36-caracteres)

**Cuándo usar:**

- Interoperabilidad con sistemas externos que requieren UUID
- Estándares específicos de tu industria
- Cuando la aleatoriedad completa es crítica
- Integración con APIs que esperan UUID
- Sistemas que ya usan UUID y no quieres migrar

**Ventajas:**

- ✅ Estándar RFC 4122 (universal)
- ✅ Ampliamente soportado
- ✅ Completamente aleatorio (seguridad)
- ✅ Compatible con bases de datos nativas UUID

**Desventajas:**

- ❌ No sortable (aleatoriedad completa)
- ❌ El más largo (36 caracteres con guiones)
- ❌ Peor performance en índices (fragmentación)
- ❌ Menos legible

**Ideal para:** Session IDs, API Keys, Integration tokens, External references

### Comparación Rápida

[](#comparación-rápida)

TipoTamañoSortableVelocidadUso Recomendado**Integer**4-8 bytes✅ Secuencial⚡⚡⚡ Muy rápidoTablas pequeñas, legacy**ULID**26 chars✅ Por tiempo⚡⚡ Rápido**Uso general (recomendado)****KSUID**27 chars✅ Por tiempo⚡⚡ RápidoTime-series, logs**UUID v4**36 chars❌ Aleatorio⚡ ModeradoInteroperabilidad, APIs externas---

Sobreescribir Columnas Heredadas
--------------------------------

[](#sobreescribir-columnas-heredadas)

Puedes personalizar las columnas heredadas en tu entidad usando el atributo `#[ORM\AttributeOverride]`:

### Cambiar nombre de columna

[](#cambiar-nombre-de-columna)

```
use PDSSUtilities\AbstractEntityModelUlid;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\AttributeOverride(
    name: 'id',
    column: new ORM\Column(name: 'product_id', type: 'string', length: 26)
)]
class Product extends AbstractEntityModelUlid
{
    // La columna 'id' ahora se llama 'product_id' en la base de datos
}
```

### Cambiar longitud del ID

[](#cambiar-longitud-del-id)

```
#[ORM\Entity]
#[ORM\AttributeOverride(
    name: 'id',
    column: new ORM\Column(name: 'id', type: 'string', length: 50)
)]
class CustomEntity extends AbstractEntityModelUlid
{
    // El ID ahora permite hasta 50 caracteres
}
```

### Cambiar nombre de columnas de timestamps

[](#cambiar-nombre-de-columnas-de-timestamps)

```
#[ORM\Entity]
#[ORM\AttributeOverrides([
    new ORM\AttributeOverride(
        name: 'created',
        column: new ORM\Column(name: 'created_at', type: 'datetimetz_immutable')
    ),
    new ORM\AttributeOverride(
        name: 'updated',
        column: new ORM\Column(name: 'updated_at', type: 'datetimetz_immutable')
    )
])]
class Article extends AbstractEntityModelUlid
{
    // Las columnas ahora se llaman 'created_at' y 'updated_at'
}
```

### Cambiar tipo de columna ID (integer)

[](#cambiar-tipo-de-columna-id-integer)

```
#[ORM\Entity]
#[ORM\AttributeOverride(
    name: 'id',
    column: new ORM\Column(name: 'id', type: 'bigint')
)]
class LargeTable extends AbstractEntityModel
{
    // El ID ahora es BIGINT en lugar de INTEGER
}
```

---

Utilidades de Query
-------------------

[](#utilidades-de-query)

### QueryFilter

[](#queryfilter)

Aplica filtros dinámicos a un QueryBuilder de Doctrine basado en parámetros HTTP.

**Operadores disponibles:**

- `EQUAL` / `=` - Igualdad
- `NOT_EQUAL` / `!=` - Diferente
- `DIFFERENT` / `` - Diferente
- `GREATER_THAN` / `>` - Mayor que
- `LESS_THAN` / `=` - Mayor o igual
- `LESS_EQUAL_THAN` / ` "AND",
        "conditionsLogic" => "AND",
        "conditions" => [
            [
                "filterOperator" => "like",
                "value" => ["single" => "John"],
                "property" => "name"
            ],
            [
                "filterOperator" => ">=",
                "value" => ["single" => 18],
                "property" => "age"
            ]
        ]
    ]
];

$qb = $entityManager->createQueryBuilder()
    ->select('u')
    ->from(User::class, 'u');

$qb = QueryFilter::addFilters($qb, $filter);
```

**Filtros con joins:**

```
$filter = [
    [
        "groupLogic" => "AND",
        "conditionsLogic" => "OR",
        "conditions" => [
            [
                "filterOperator" => "=",
                "value" => ["single" => "active"],
                "property" => "status",
                "onJoinedProperty" => "profile"  // Aplicar filtro en tabla relacionada
            ]
        ]
    ]
];
```

**Filtros compuestos:**

```
$filter = [
    [
        "groupLogic" => "AND",
        "conditionsLogic" => "AND",
        "conditions" => [
            [
                "filterOperator" => "=",
                "value" => ["single" => "admin"],
                "property" => "role"
            ]
        ],
        "compoundConditions" => [
            [
                "conditionsLogic" => "OR",
                "conditions" => [
                    [
                        "filterOperator" => "like",
                        "value" => ["single" => "%@example.com"],
                        "property" => "email"
                    ],
                    [
                        "filterOperator" => "like",
                        "value" => ["single" => "%@test.com"],
                        "property" => "email"
                    ]
                ],
                "compoundConditions" => []
            ]
        ]
    ]
];
```

### QueryJoins

[](#queryjoins)

Agrega joins dinámicos a un QueryBuilder.

**Tipos de join:**

- `LEFT` - LEFT JOIN (predeterminado)
- `INNER` - INNER JOIN

**Ejemplo:**

```
use PDSSUtilities\QueryJoins;

$joins = [
    [
        "joinType" => "LEFT",
        "alias" => "profile",
        "property" => "profile"
    ],
    [
        "joinType" => "INNER",
        "alias" => "orders",
        "property" => "orders"
    ],
    [
        "joinType" => "LEFT",
        "alias" => "items",
        "property" => "items",
        "joinedProperty" => "orders"  // Join desde otro join
    ]
];

$qb = $entityManager->createQueryBuilder()
    ->select('u')
    ->from(User::class, 'u');

$qb = QueryJoins::addJoins($qb, $joins);
// Resultado: FROM User u LEFT JOIN u.profile profile INNER JOIN u.orders orders LEFT JOIN orders.items items
```

### QuerySelect

[](#queryselect)

Calcula el valor SELECT para queries con selección parcial de propiedades.

**Ejemplo:**

```
use PDSSUtilities\QuerySelect;

$select = [
    'properties' => ['id', 'name', 'email'],  // Propiedades de la entidad raíz
    'joins' => [
        [
            'joinedAlias' => 'profile',
            'properties' => ['id', 'bio', 'avatar']
        ],
        [
            'joinedAlias' => 'orders',
            'properties' => ['id', 'total', 'status']
        ]
    ]
];

$selectValues = QuerySelect::createDoctrineSelectValue('u', $select);
// Resultado: ['partial u.{id,name,email}', 'partial profile.{id,bio,avatar}', 'partial orders.{id,total,status}']

$qb->select($selectValues);
```

**Sin propiedades específicas:**

```
$select = [
    'properties' => [],  // Selecciona todo de la raíz
    'joins' => [
        [
            'joinedAlias' => 'profile',
            'properties' => []  // Selecciona todo del join
        ]
    ]
];

$selectValues = QuerySelect::createDoctrineSelectValue('u', $select);
// Resultado: ['u', 'profile']
```

### QuerySort

[](#querysort)

Aplica ordenamiento dinámico a un QueryBuilder.

**Direcciones:**

- `asc` - Ascendente
- `desc` - Descendente

**Ejemplo:**

```
use PDSSUtilities\QuerySort;

$orderBy = [
    [
        "direction" => "desc",
        "property" => "created"
    ],
    [
        "direction" => "asc",
        "property" => "name"
    ]
];

$qb = $entityManager->createQueryBuilder()
    ->select('u')
    ->from(User::class, 'u');

$qb = QuerySort::addOrderBy($qb, $orderBy);
// Resultado: ORDER BY u.created DESC, u.name ASC
```

**Ordenar por propiedades de joins:**

```
$orderBy = [
    [
        "direction" => "desc",
        "property" => "createdAt",
        "onJoinedProperty" => "orders"  // Ordenar por campo de tabla relacionada
    ]
];
```

**Desde string JSON:**

```
$orderByJson = '[{"direction":"desc","property":"created"}]';
$orderBy = QuerySort::standardizeRequestParams($orderByJson);
$qb = QuerySort::addOrderBy($qb, $orderBy);
```

---

Seguridad en Queries Dinámicas
------------------------------

[](#seguridad-en-queries-dinámicas)

### Datos desde HTTP POST/GET

[](#datos-desde-http-postget)

Las utilidades `QueryFilter`, `QueryJoins`, `QuerySelect` y `QuerySort` están diseñadas para recibir datos desde peticiones HTTP (POST, GET, etc.). Es **fundamental** aplicar las validaciones de seguridad adecuadas.

**Ejemplo de uso con datos POST:**

```
// Controlador recibiendo datos POST
$requestData = json_decode(file_get_contents('php://input'), true);

$filters = $requestData['filters'] ?? [];
$joins = $requestData['joins'] ?? [];
$select = $requestData['select'] ?? [];
$orderBy = $requestData['orderBy'] ?? [];

// Aplicar a QueryBuilder
$qb = $entityManager->createQueryBuilder()
    ->select('u')
    ->from(User::class, 'u');

$qb = QueryJoins::addJoins($qb, $joins);
$qb = QueryFilter::addFilters($qb, $filters);
$qb = QuerySort::addOrderBy($qb, $orderBy);
```

### Protección de Doctrine contra SQL Injection

[](#protección-de-doctrine-contra-sql-injection)

Doctrine ORM aplica **prepared statements y parameter binding automáticamente**, lo que protege contra inyección SQL:

```
// Doctrine convierte esto:
$qb->where('u.name = :name')->setParameter('name', $userInput);

// En un prepared statement:
// SELECT * FROM users WHERE name = ?
// Binding: ['John']
```

**✅ Seguro por defecto:**

- Todos los valores en `QueryFilter` se pasan como parámetros vinculados
- Los operadores están validados contra constantes de la clase
- Las propiedades se concatenan como identificadores, no valores

### Validaciones de Seguridad Recomendadas

[](#validaciones-de-seguridad-recomendadas)

#### 1. **Whitelist de propiedades permitidas**

[](#1-whitelist-de-propiedades-permitidas)

```
class QueryValidator
{
    private const ALLOWED_PROPERTIES = [
        'User' => ['id', 'name', 'email', 'created', 'updated'],
        'Profile' => ['id', 'bio', 'avatar'],
        'Order' => ['id', 'total', 'status', 'created']
    ];

    public static function validateProperty(string $entity, string $property): bool
    {
        return in_array($property, self::ALLOWED_PROPERTIES[$entity] ?? [], true);
    }

    public static function validateFilters(array $filters, string $entity): void
    {
        foreach ($filters as $group) {
            foreach ($group['conditions'] ?? [] as $condition) {
                if (!self::validateProperty($entity, $condition['property'])) {
                    throw new \InvalidArgumentException("Propiedad no permitida: {$condition['property']}");
                }
            }
        }
    }
}

// Uso:
try {
    QueryValidator::validateFilters($filters, 'User');
    $qb = QueryFilter::addFilters($qb, $filters);
} catch (\InvalidArgumentException $e) {
    // Manejar error de validación
}
```

#### 2. **Whitelist de joins permitidos**

[](#2-whitelist-de-joins-permitidos)

```
class QueryValidator
{
    private const ALLOWED_JOINS = [
        'User' => ['profile', 'orders', 'roles'],
        'Order' => ['items', 'user'],
    ];

    public static function validateJoins(array $joins, string $entity): void
    {
        foreach ($joins as $join) {
            $property = $join['property'];
            if (!in_array($property, self::ALLOWED_JOINS[$entity] ?? [], true)) {
                throw new \InvalidArgumentException("Join no permitido: {$property}");
            }
        }
    }
}
```

#### 3. **Limitar operadores permitidos**

[](#3-limitar-operadores-permitidos)

```
class QueryValidator
{
    private const ALLOWED_OPERATORS = [
        'public' => ['=', 'like', '>', '=', '', '=', '
