
Compartir:
Actor de formación con una disertación sobre la comedia, llegué al desarrollo de PHP a través de la escena de las reuniones. Puedes encontrarme hablando y escribiendo sobre tecnología, o tocando/comprando discos raros de mi colección de vinilos.
Seguridad Tipográfica Bien Hecha - PHP Array Hacking
Tiempo de lectura: 6 minutos
En los primeros cinco o seis años de mi experiencia en el desarrollo de aplicaciones PHP a gran escala, experimenté y creé muchos de los problemas que la gente que empieza en el desarrollo de backend habrá visto a menudo. Es sólo ahora, especialmente después de heredar el Vonage PHP SDKque me he dado cuenta de que estoy en una posición en la que puedo educar a otros para que no hagan lo mismo. Estos problemas en realidad se derivan mucho de cómo he visto PHP en particular a lo largo de los años, independientemente de si se utiliza un framework o no. Lancemos la primera granada:
Los Arrays PHP son un poco horribles
Vale, no son horribles, eso sería algo extraño de decir. Lo que quiero decir es, la forma en que se utilizan a menudo es horrible. Hay una muy buena razón para esto; comenzando con PHP como mi primer lenguaje (después de VBA y SQL) no estaba al tanto de cómo otros lenguajes manejan colecciones de datos. Simplemente asumí que eran iguales a PHP:
Tienes una matriz de índices, que tiene una clave numérica o
Tiene un array asociado, que tiene definidas claves alfanuméricas.
Ambas se consideran el mismo tipo de variable, es decir arrayporque el motor ha resuelto qué tipo de array se quiere crear.
$myAssociatedArray = ['foo' => 'bar']; // array
$myHashedArray = ['foo', 'bar']; // also array
Así es como probablemente funciona todo lo demás, ¿verdad?
No, no lo es. Mi ingenuidad con el uso de PHP sólo ha significado que yo no era consciente de la siguiente declaración:
En todos los demás lenguajes backend importantes, las matrices indexadas y asociativas se dividen en dos o más clases base diferentes.
Además, Arrays Asociativos es un término usado sólo en PHP. De ahora en adelante, llamaremos matrices asociativas matrices hashed porque es el término más usado en otros sitios.
Este es el comportamiento para otros idiomas:
NodeJS
Array(matriz indexada)Object(matriz con hash)
Python
List(matriz indexada)Dictionary(hashed array), utilizando List Comprehension
Ruby
Array(matriz indexada)Hash(matriz con hash)
Vale la pena notar que Ruby puede tener un arreglo asociativo de la misma manera que PHP, pero debido a la existencia del objeto Hash es muy raro ver un arreglo siendo usado como hash.
Ir(lang)
Slice(matriz indexada)Map(matriz con hash)
Java
Array(matriz indexada, primitiva)List(interfaz, implementada porArrayList)ArrayList(matriz indexada, crea un nuevo objeto para el dimensionamiento)Hashmap(matriz con hash, implementación deMap)LinkedHashMap(matriz con hash, reordena el orden de inserción)
Vale la pena señalar que, como puedes ver, Java es -muy- estricto con los conjuntos de estructura de datos. Todos ellos son mutables en el sentido de que puedes cambiar sus valores, pero no puedes cambiar el tamaño del objeto con respecto a sus nodos. Por eso en ArrayListporque en realidad crea un nuevo objeto al modificarlo.
C#
Array(matriz indexada)List(matriz indexada con genéricos, mutable)Dictionary(matriz con hash)
Así que, dado el hecho de que todas estas clases diferentes en la API base de esos lenguajes, significa que todos ellos tienen la capacidad de inculcar un comportamiento de tipo más estricto.
¿Y qué?
No tenemos eso en PHP. Esto significa que que los arreglos PHP pueden ser cualquier cosa. Es este hecho el que ha resultado en el tipo de código que he estado leyendo durante años que se parece a esto:
public function updateRecord(int $id, array $options) {
// do some stuff here
}Quiero actualizar el ID de algo. Y entonces... Tengo que $options. Porque son opciones. Pero es un simple array PHP. ¿Qué hay dentro? La función no tiene ni idea.
¿Qué tiene de malo este código? Si la experiencia personal sirve de algo, cuatro horas de xdebug para averiguar de dónde viene una clave de array. No tenemos ni idea de cómo se supone que debe ser. Este particular es algo que solía codificar yo mismo durante años, especialmente en entornos de agencia donde el tiempo es oro. Si la única preocupación es "¿Funciona de acuerdo con los requisitos del cliente?", entonces se tomarán todas las medidas posibles para entregar el producto a tiempo. ¿La persona que venga después de ti y tenga que arreglar un error provocado por un efecto secundario? Eso es deuda tecnológica.
Bien, ¿y ahora qué?
El camino que he recorrido para mantener el código es utilizar sugerencias de tipo en todas partes, todo el tiempo. Este es el tipo de desarrollo de software que, si se hace a lo largo y a fondo, debería debería resultar en el mítico código auto-documentado. Pero, sólo podemos escribir hint an array, así que eso no nos ayuda aquí. Así que tenemos tres opciones:
Genéricos
Forma de matriz (mediante Generics pirateados)
Objetos de valor
Genéricos
Lo siento, te he engañado: no tenemos genéricos en PHP debido a limitaciones dentro del motor. Sin embargo, como puede que veas la palabra un poco manoseada, considera el siguiente código de TypeScript:
interface UserProfile {
id: string;
name: string;
age: integer;
}
class Collection<Type> {
private items: Type[] = [];
add(item: Type) {
this.items.push(item);
}
get(index: number): Type | undefined {
return this.items[index];
}
}
const userProfiles = new Collection<UserProfile>();
Como alguien que está muy en el campo de "¡haz este PHP tan robusto como sea posible, hazlo más duro que el Acero Valyrio!", genéricos como este son una profunda alegría para trabajar. El <> indican que al crear un nuevo objeto Collection debe debe contener sólo el tipo dado durante su instanciación. Así, cuando creamos un nuevo objeto Collection<UserProfile>estamos diciendo que este objeto sólo puede contener objetos del tipo UserProfile objetos. Queamigos y enemigos, es código autodocumentado.
¿Puede PHP hacer esto de otra manera?
Me alegro de que lo preguntes, ¡sí se puede! Esto nos va a llevar al punto número dos, que es una definición de matriz. Esto suena tan estúpido para mí decir esto, pero hasta que me uní a Vonage nunca se me había ocurrido que podía extender fuera de núcleo SPL dentro de la API de PHP. Asi que puedes extender arrays¡!
class Collection extends \ArrayObject {
protected function __construct(...$args) {
parent::__construct(...args);
}
}Actualmente, esto no hace nada aparte de pasar los argumentos a la clase padre con el operador splat, que es un operador ArrayObject. Pero, lo hemos extendido, ¡lo que significa que podemos sobrecargar el comportamiento!
Tenemos que hacer dos cosas aquí:
Establecer un tipo, de la misma manera que lo hicimos en TypeScript
Sobrecarga el método
offsetSet()que es el método SPL para añadir a un array (ya sea cuando se añade un valor después de la instanciación o durante la creación del constructor).
Así que, abordemos esto:
class Collection extends \ArrayObject {
protected $typeError = 'Only UserProfile objects can be added!'
public function __construct(...$args) {
foreach ($args as $arg) {
if (!$arg instanceof UserProfile) {
throw new \TypeError($this->typeError);
}
}
parent::__construct(...$args);
}
public function offsetSet($key, $value): void
{
if (!$value instanceof UserProfile) {
throw new \TypeError($this->typeError);
}
parent::offsetSet($key, $value);
}
}
Y ahí lo tenemos. offsetSet está sobrecargado para que sólo se puedan añadir objetos UserProfile y al crear el objeto Collection puedes pasar cualquier número de argumentos siempre que sean UserProfile objetos.
Objetos de valor
Hay una solución mucho más simple al problema de lanzar arrays: no lo hagas. He continuado el trabajo realizado por mis predecesores en el SDK de Vonage PHP Core para asegurarme de que cualquier cosa que se pase a los métodos del Cliente es un objeto de valor.
Sí, tiene sus inconvenientes. Significa que hay que definir las propiedades de la clase, que hay que añadir métodos getter y setter. Significa más código. Sin embargo, también tenemos herramientas para hacer que el código sea menos hinchado en el PHP moderno, con el uso de clases nativas o la promoción de propiedades del constructor con modificadores de acceso para hacer que el código sea menos hinchado. enum o la promoción de propiedades del constructor con Modificadores de Acceso para mantener el número de líneas bajo.
Un buen ejemplo de esto fue cuando codifiqué la implementación del SDK de PHP de Verify v2. El envío de una solicitud de verificación se vería así sin objetos de valor:
$payload = [
'locale' => 'en_us',
'channel_timeout' => 300,
'client_ref' => 'a-reference',
'code_length' => 4,
'workflow' => [
'channel' => 'sms',
'to' => '123456789'
]
];
$myVonageClient = new Client('apiKey', 'apiSecret');
$myVonageClient->verify2()->startVerification($payload);
El objeto cliente no sabe nada de la matriz que se le pasa. ¿Qué pasa si una clave es incorrecta? ¿Qué pasa si falta una clave? No hay nada dentro del código que le diga sobre el comportamiento: y si yo hizo validación en el método startVerification() tendrías que rebuscar en mi código para averiguar lo que tenía en la cabeza.
En su lugar, utilizando objetos de valor, trasladamos la lógica a PHP bien tipado. Se vuelve auto-documentado y endurecido por diseño.
class SMSRequest extends BaseVerifyRequest
{
public function __construct(
protected string $to,
protected string $brand,
protected ?VerificationLocale $locale = null,
) {
if (!$this->locale) {
$this->locale = new VerificationLocale();
}
$workflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_SMS, $to);
$this->addWorkflow($workflow);
}
}
Este objeto se pasa ahora al método startVerification() método. Lo que se puede y no se puede hacer en los objetos pasados ahora se puede definir en el nivel más bajo del código. Por ejemplo, aquí está el objeto VerificationLocale objeto.
class VerificationLocale
{
private array $allowedCodes = [
'en-us',
'en-gb',
'es-es',
'es-mx',
'es-us',
'it-it',
'fr-fr',
'de-de',
'ru-ru',
'hi-in',
'pt-br',
'pt-pt',
'id-id',
];
public function __construct(protected string $code = 'en-us')
{
if (! in_array($code, $this->allowedCodes, true)) {
throw new \InvalidArgumentException('Invalid Locale Code Provided');
}
}
public function getCode(): string
{
return $this->code;
}
public function setCode(string $code): static
{
$this->code = $code;
return $this;
}
}
Y ahí lo tenemos. Código auto-documentado. Sí, tienes que crear todos los objetos para coserlos todos juntos, pero también cómo se comporta nuestro SDK es estricto y explícito.
Compartir:
Actor de formación con una disertación sobre la comedia, llegué al desarrollo de PHP a través de la escena de las reuniones. Puedes encontrarme hablando y escribiendo sobre tecnología, o tocando/comprando discos raros de mi colección de vinilos.