Algunos artículos que pueden ayudarte:
Categoría: Cómo hacer para…
Estos artículos te explicarán cómo resolver problemas específicos usando PHP
-

Introducir objetos en un código viejo
Seguramente a vos nunca te haya pasado lo que te voy a contar, pero tenés algún amigo al que sí.
Uno de esos pobres programadores que se enfrenta a una codebase que combina cosas como:
function shipTo(string $street, int $number, string $postalCode, string $city): void; function getDeliveryCost(string $city, string $street, int $number, string $postalCode): float; function canPurchase(string $city, string $postalCode, int $number, string $street): bool;
Claramente, este tipo de estructuras son una invitación al error.
A partir de PHP 8.0 existen los argumentos nombrados, con lo cual el problema está de algún modo mitigado aunque, en mi opinión, es más un workaround que una solución como tal.
Un mejor enfoque sería tomar una mirada algo menos pegada al código y más cercana al dominio y darse cuenta de que aquí hay un concepto implícito: la dirección postal.
Lo correcto en esta situación sería implementar una clase que modele este concepto de dominio:
<?php declare(strict_types=1); readonly class Address { public function __construct( private string $street, private int $number, private string $postalCode, private string $city ) { } public function __toString(): string { return "$this->street $this->number, $this->postalCode, $this->city"; } }Y luego hacer que las funciones usen instancias de este objeto:
function shipTo(Address $address): void; function getDeliveryCost(Address $address): float; function canPurchase(Address $address): bool;
Ahora sí el código de tu amigo se ve mucho más bonito, limpio, mantenible y profesional, ¿cierto?
Pero… ¿cómo puede tu amigo implementar este cambio sin romper todo el código ya está escrito usando las firmas viejas?
Paso intermedio: wrappers al rescate
Aunque parezca una tarea titánica (y ciertamente lo es si se intenta hacer en un solo paso), es muy sencillo realizar el cambio en forma segura. Sólo requiere un poquito de planificación.
La clave está en crear un intermediario que haga de adaptador entre el código viejo y el nuevo.
Tomemos por ejemplo la función
shipTo. Queremos pasar de esta firmafunction shipTo(string $street, int $number, string $postalCode, string $city): voida esta otra:function shipTo(Address $address): voidpero, si simplemente hacemos el cambio, todas las llamadas a la vieja comenzarán a fallar.Lo que podemos hacer es:
- Crear la nueva función con la firma objetivo (
function shipTo(Address $address): void) - Copiar el cuerpo de la función vieja a la nueva
- Adaptar el código de la nueva función para que use objetos
Addressen lugar de parámetros individuales - Cambiar el código en la función original por una llamada a la nueva implementación, pasándole como parámetro una nueva instancia de Address creada a partir de los parámetros recibidos:
function shipTo(string $street, int $number, string $postalCode, string $city): void { shipTo(new Address($street, $number, $postalCode, $city)); }- Marcar el método (o función) viejo como
#[Deprecated](O@Deprecatedsi tu versión de PHP no soporta atributos), de modo que, en lo sucesivo, todo el equipo sepa que debe usar el método nuevo.
Una vez hecho todo esto será cuestión de tiempo hasta que todo el código haya sido migrado hacia la implementación que usa objetos.
Un detalle importante:
Si prestaste atención abrás notado que hay dos funciones con el mismo nombre:
shipTo.Desafortunadamente, PHP no soporta la sobrecarga de funciones como otros lenguajes (Java por ejemplo) lo cual hará que, para que todo esto funcione, sea necesario ensuciar un poquito el código usando un nombre diferente para la nueva función, por ejemplo
shipToAddress.No es lo que más me gusta pero lo considero un precio aceptable.
Así que ahora sí podés ir a contarle a tu amigo cómo hacer para dejar de sufrir ese código que vaya uno a saber quién escribió.
- Crear la nueva función con la firma objetivo (
-

Cómo enviar encabezados SOAP desde PHP
El protocolo SOAP, a pesar de lo que indica su nombre, es de todo menos sencillo.
Principalmente, su complejidad deriva del hecho de estar basado en XML, aunque no es lo único que tiene.
En teoría, es un protocolo super flexible. En la realidad… un dolor de cabeza importante.
En PHP existen varias implementaciones que intentan simplificar un poco el problema.
Las veces que me ha tocado enfrentarme a SOAP las clases SoapClient y SoapServer han estado a la altura, aunque cuando aparecieron los encabezados no fue tan sencillo.
Tomemos como ejemplo este servicio web que define el siguiente WSDL:
<?xml version="1.0" encoding="utf-8"?> <wsdl:definitions xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="https://sccnlp.com/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" targetNamespace="https://sccnlp.com/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"> <wsdl:types> <s:schema elementFormDefault="qualified" targetNamespace="https://sccnlp.com/"> <s:element name="registrarNombradas"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombradas" type="tns:ArrayOfNombrada" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="ArrayOfNombrada"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="Nombrada" nillable="true" type="tns:Nombrada" /> </s:sequence> </s:complexType> <s:complexType name="Nombrada"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="idPuerto" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idTurno" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="idNave" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idLocacion" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="trabajadores" type="tns:ArrayOfTrabajadorNombrada" /> </s:sequence> </s:complexType> <s:complexType name="ArrayOfTrabajadorNombrada"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="TrabajadorNombrada" nillable="true" type="tns:TrabajadorNombrada" /> </s:sequence> </s:complexType> <s:complexType name="TrabajadorNombrada"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idContrato" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idLabor" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idFuncion" type="s:int" /> </s:sequence> </s:complexType> <s:element name="registrarNombradasResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="registrarNombradasResult" type="tns:NombradaCreada" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="NombradaCreada"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="nombradaCreadaDetalle" type="tns:ArrayOfNombradaCreadaDetalle" /> </s:sequence> </s:complexType> <s:complexType name="ArrayOfNombradaCreadaDetalle"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="NombradaCreadaDetalle" nillable="true" type="tns:NombradaCreadaDetalle" /> </s:sequence> </s:complexType> <s:complexType name="NombradaCreadaDetalle"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="id" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="error" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idEstado" type="s:int" /> </s:sequence> </s:complexType> <s:element name="UserCredentials" type="tns:UserCredentials" /> <s:complexType name="UserCredentials"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="userName" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="password" nillable="true" type="s:string" /> </s:sequence> <s:anyAttribute /> </s:complexType> <s:element name="modificarNombradas"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="lista" type="tns:ArrayOfTrabajadorNombradaEdicion" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="ArrayOfTrabajadorNombradaEdicion"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="TrabajadorNombradaEdicion" nillable="true" type="tns:TrabajadorNombradaEdicion" /> </s:sequence> </s:complexType> <s:complexType name="TrabajadorNombradaEdicion"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idContrato" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="idContratoNuevo" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idLabor" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idFuncion" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="activo" type="s:boolean" /> <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="dvTrabajador" nillable="true" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="pasaporte" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="extensionJornada" type="s:boolean" /> </s:sequence> </s:complexType> <s:element name="modificarNombradasResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="modificarNombradasResult" type="tns:NombradaCreada" /> </s:sequence> </s:complexType> </s:element> <s:element name="getResolucionNombrada"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> </s:sequence> </s:complexType> </s:element> <s:element name="getResolucionNombradaResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="getResolucionNombradaResult" type="tns:NombradaResolucion" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="NombradaResolucion"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="nombradaResolucionDetalle" type="tns:NombradaResolucionDetalle" /> </s:sequence> </s:complexType> <s:complexType name="NombradaResolucionDetalle"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="id" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idEmpresa" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" nillable="true" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombrada" nillable="true" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="idTurno" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Turno" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idNave" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Nave" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idLocacion" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="lugar" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="posicion" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="fechaCreacion" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="activo" nillable="true" type="s:boolean" /> <s:element minOccurs="1" maxOccurs="1" name="idEstadoNombrada" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoNombrada" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="trabajadores" type="tns:ArrayOfTrabajadorResolucion" /> </s:sequence> </s:complexType> <s:complexType name="ArrayOfTrabajadorResolucion"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="TrabajadorResolucion" nillable="true" type="tns:TrabajadorResolucion" /> </s:sequence> </s:complexType> <s:complexType name="TrabajadorResolucion"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idContrato" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idContratoNuevo" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idTrabajador" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="pasaporte" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="nombres" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="apellidos" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idLabor" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idFuncion" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="estadoTrabajador" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="activo" type="s:boolean" /> <s:element minOccurs="1" maxOccurs="1" name="idEstadoTrabajador" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="extensionJornada" type="s:boolean" /> <s:element minOccurs="1" maxOccurs="1" name="horasExtras" nillable="true" type="s:double" /> <s:element minOccurs="1" maxOccurs="1" name="fechaCreacion" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombradaTrabajador" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="fechaModificacion" nillable="true" type="s:dateTime" /> </s:sequence> </s:complexType> <s:element name="consultarNombrada"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="filtro" nillable="true" type="tns:FiltroNombrada" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="FiltroNombrada"> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="idNombrada" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="fechaInicio" nillable="true" type="s:dateTime" /> <s:element minOccurs="0" maxOccurs="1" name="idLabor" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="idNave" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="idLocacion" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="rutTrabajador" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="pasaporte" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="idEstado" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="idFuncion" nillable="true" type="s:int" /> </s:sequence> </s:complexType> <s:element name="consultarNombradaResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="consultarNombradaResult" type="tns:NombradaConsultaMuellaje" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="NombradaConsultaMuellaje"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="consultaDetalle" type="tns:ArrayOfNombradaOutMuellaje" /> </s:sequence> </s:complexType> <s:complexType name="ArrayOfNombradaOutMuellaje"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="NombradaOutMuellaje" nillable="true" type="tns:NombradaOutMuellaje" /> </s:sequence> </s:complexType> <s:complexType name="NombradaOutMuellaje"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idNombrada" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombrada" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="idNave" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombreNave" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idPuerto" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombrePuerto" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idLocacion" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="lugar" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="posicion" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idTurno" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaTurno" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idEstadoNombrada" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoNombrada" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="rutMuellaje" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombreMuellaje" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="rutConcesionaria" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombreConcesionario" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="NOMBRE_INSTALACION" type="s:string" /> </s:sequence> </s:complexType> <s:element name="eliminarNombrada"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" /> </s:sequence> </s:complexType> </s:element> <s:element name="eliminarNombradaResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="eliminarNombradaResult" type="tns:Respuesta" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="Respuesta"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" /> </s:sequence> </s:complexType> <s:element name="consultarNombradaByConcesionaria"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="filtro" nillable="true" type="tns:FiltroNombrada" /> </s:sequence> </s:complexType> </s:element> <s:element name="consultarNombradaByConcesionariaResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="consultarNombradaByConcesionariaResult" type="tns:NombradaConsultaConcesionario" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="NombradaConsultaConcesionario"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="consultaDetalle" type="tns:ArrayOfNombradaOutConcesionario" /> </s:sequence> </s:complexType> <s:complexType name="ArrayOfNombradaOutConcesionario"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="NombradaOutConcesionario" nillable="true" type="tns:NombradaOutConcesionario" /> </s:sequence> </s:complexType> <s:complexType name="NombradaOutConcesionario"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idNombrada" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombrada" type="s:dateTime" /> <s:element minOccurs="1" maxOccurs="1" name="idNave" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombreNave" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idLocacion" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="lugar" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idPuerto" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombrePuerto" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idTurno" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaTurno" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="posicion" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idEstadoNombrada" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoNombrada" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="rutMuellaje" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombreMuellaje" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idLabor" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="idFuncion" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="rutConcesionaria" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="nombreConcesionaria" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="trabajadores" type="tns:ArrayOfTrabajador" /> </s:sequence> </s:complexType> <s:complexType name="ArrayOfTrabajador"> <s:sequence> <s:element minOccurs="0" maxOccurs="unbounded" name="Trabajador" nillable="true" type="tns:Trabajador" /> </s:sequence> </s:complexType> <s:complexType name="Trabajador"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="idContrato" nillable="true" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="pasaporteTrabajador" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="nombresTrabajador" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="apellidoPaternoTrabajador" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idLabor" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaLabor" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="idFuncion" nillable="true" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="glosaFuncion" type="s:string" /> <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoTrabajador" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="activoTrabajador" nillable="true" type="s:boolean" /> </s:sequence> </s:complexType> <s:element name="verificarTrabajadorPortuario"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" type="s:int" /> <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="fecha" type="s:dateTime" /> </s:sequence> </s:complexType> </s:element> <s:element name="verificarTrabajadorPortuarioResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="verificarTrabajadorPortuarioResult" type="tns:Respuesta" /> </s:sequence> </s:complexType> </s:element> </s:schema> </wsdl:types> <wsdl:message name="registrarNombradasSoapIn"> <wsdl:part name="parameters" element="tns:registrarNombradas" /> </wsdl:message> <wsdl:message name="registrarNombradasSoapOut"> <wsdl:part name="parameters" element="tns:registrarNombradasResponse" /> </wsdl:message> <wsdl:message name="registrarNombradasUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:message name="modificarNombradasSoapIn"> <wsdl:part name="parameters" element="tns:modificarNombradas" /> </wsdl:message> <wsdl:message name="modificarNombradasSoapOut"> <wsdl:part name="parameters" element="tns:modificarNombradasResponse" /> </wsdl:message> <wsdl:message name="modificarNombradasUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:message name="getResolucionNombradaSoapIn"> <wsdl:part name="parameters" element="tns:getResolucionNombrada" /> </wsdl:message> <wsdl:message name="getResolucionNombradaSoapOut"> <wsdl:part name="parameters" element="tns:getResolucionNombradaResponse" /> </wsdl:message> <wsdl:message name="getResolucionNombradaUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:message name="consultarNombradaSoapIn"> <wsdl:part name="parameters" element="tns:consultarNombrada" /> </wsdl:message> <wsdl:message name="consultarNombradaSoapOut"> <wsdl:part name="parameters" element="tns:consultarNombradaResponse" /> </wsdl:message> <wsdl:message name="consultarNombradaUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:message name="eliminarNombradaSoapIn"> <wsdl:part name="parameters" element="tns:eliminarNombrada" /> </wsdl:message> <wsdl:message name="eliminarNombradaSoapOut"> <wsdl:part name="parameters" element="tns:eliminarNombradaResponse" /> </wsdl:message> <wsdl:message name="eliminarNombradaUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:message name="consultarNombradaByConcesionariaSoapIn"> <wsdl:part name="parameters" element="tns:consultarNombradaByConcesionaria" /> </wsdl:message> <wsdl:message name="consultarNombradaByConcesionariaSoapOut"> <wsdl:part name="parameters" element="tns:consultarNombradaByConcesionariaResponse" /> </wsdl:message> <wsdl:message name="consultarNombradaByConcesionariaUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:message name="verificarTrabajadorPortuarioSoapIn"> <wsdl:part name="parameters" element="tns:verificarTrabajadorPortuario" /> </wsdl:message> <wsdl:message name="verificarTrabajadorPortuarioSoapOut"> <wsdl:part name="parameters" element="tns:verificarTrabajadorPortuarioResponse" /> </wsdl:message> <wsdl:message name="verificarTrabajadorPortuarioUserCredentials"> <wsdl:part name="UserCredentials" element="tns:UserCredentials" /> </wsdl:message> <wsdl:portType name="NombradasSoap"> <wsdl:operation name="registrarNombradas"> <wsdl:input message="tns:registrarNombradasSoapIn" /> <wsdl:output message="tns:registrarNombradasSoapOut" /> </wsdl:operation> <wsdl:operation name="modificarNombradas"> <wsdl:input message="tns:modificarNombradasSoapIn" /> <wsdl:output message="tns:modificarNombradasSoapOut" /> </wsdl:operation> <wsdl:operation name="getResolucionNombrada"> <wsdl:input message="tns:getResolucionNombradaSoapIn" /> <wsdl:output message="tns:getResolucionNombradaSoapOut" /> </wsdl:operation> <wsdl:operation name="consultarNombrada"> <wsdl:input message="tns:consultarNombradaSoapIn" /> <wsdl:output message="tns:consultarNombradaSoapOut" /> </wsdl:operation> <wsdl:operation name="eliminarNombrada"> <wsdl:input message="tns:eliminarNombradaSoapIn" /> <wsdl:output message="tns:eliminarNombradaSoapOut" /> </wsdl:operation> <wsdl:operation name="consultarNombradaByConcesionaria"> <wsdl:input message="tns:consultarNombradaByConcesionariaSoapIn" /> <wsdl:output message="tns:consultarNombradaByConcesionariaSoapOut" /> </wsdl:operation> <wsdl:operation name="verificarTrabajadorPortuario"> <wsdl:input message="tns:verificarTrabajadorPortuarioSoapIn" /> <wsdl:output message="tns:verificarTrabajadorPortuarioSoapOut" /> </wsdl:operation> </wsdl:portType> <wsdl:binding name="NombradasSoap" type="tns:NombradasSoap"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" /> <wsdl:operation name="registrarNombradas"> <soap:operation soapAction="https://sccnlp.com/registrarNombradas" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:registrarNombradasUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="modificarNombradas"> <soap:operation soapAction="https://sccnlp.com/modificarNombradas" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:modificarNombradasUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="getResolucionNombrada"> <soap:operation soapAction="https://sccnlp.com/getResolucionNombrada" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:getResolucionNombradaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="consultarNombrada"> <soap:operation soapAction="https://sccnlp.com/consultarNombrada" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:consultarNombradaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="eliminarNombrada"> <soap:operation soapAction="https://sccnlp.com/eliminarNombrada" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:eliminarNombradaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="consultarNombradaByConcesionaria"> <soap:operation soapAction="https://sccnlp.com/consultarNombradaByConcesionaria" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:consultarNombradaByConcesionariaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="verificarTrabajadorPortuario"> <soap:operation soapAction="https://sccnlp.com/verificarTrabajadorPortuario" style="document" /> <wsdl:input> <soap:body use="literal" /> <soap:header message="tns:verificarTrabajadorPortuarioUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:binding name="NombradasSoap12" type="tns:NombradasSoap"> <soap12:binding transport="http://schemas.xmlsoap.org/soap/http" /> <wsdl:operation name="registrarNombradas"> <soap12:operation soapAction="https://sccnlp.com/registrarNombradas" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:registrarNombradasUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="modificarNombradas"> <soap12:operation soapAction="https://sccnlp.com/modificarNombradas" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:modificarNombradasUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="getResolucionNombrada"> <soap12:operation soapAction="https://sccnlp.com/getResolucionNombrada" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:getResolucionNombradaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="consultarNombrada"> <soap12:operation soapAction="https://sccnlp.com/consultarNombrada" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:consultarNombradaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="eliminarNombrada"> <soap12:operation soapAction="https://sccnlp.com/eliminarNombrada" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:eliminarNombradaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="consultarNombradaByConcesionaria"> <soap12:operation soapAction="https://sccnlp.com/consultarNombradaByConcesionaria" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:consultarNombradaByConcesionariaUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="verificarTrabajadorPortuario"> <soap12:operation soapAction="https://sccnlp.com/verificarTrabajadorPortuario" style="document" /> <wsdl:input> <soap12:body use="literal" /> <soap12:header message="tns:verificarTrabajadorPortuarioUserCredentials" part="UserCredentials" use="literal" /> </wsdl:input> <wsdl:output> <soap12:body use="literal" /> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="Nombradas"> <wsdl:port name="NombradasSoap" binding="tns:NombradasSoap"> <soap:address location="https://sccnlpservices-piloto.dirtrab.cl/Servicios/Nombradas.asmx" /> </wsdl:port> <wsdl:port name="NombradasSoap12" binding="tns:NombradasSoap12"> <soap12:address location="https://sccnlpservices-piloto.dirtrab.cl/Servicios/Nombradas.asmx" /> </wsdl:port> </wsdl:service> </wsdl:definitions>Con esta información puedes armar un script como este:
<?php $url = "https://sccnlpservices-piloto.dirtrab.cl/Servicios/Nombradas.asmx?WSDL"; $client = new SoapClient($url, ["trace" => 1, "exception" => 0]); print_r($client->__getFunctions());
Y, al correrlo obtendrás algo como:
Array ( [0] => registrarNombradasResponse registrarNombradas(registrarNombradas $parameters) [1] => modificarNombradasResponse modificarNombradas(modificarNombradas $parameters) [2] => getResolucionNombradaResponse getResolucionNombrada(getResolucionNombrada $parameters) [3] => consultarNombradaResponse consultarNombrada(consultarNombrada $parameters) [4] => eliminarNombradaResponse eliminarNombrada(eliminarNombrada $parameters) [5] => consultarNombradaByConcesionariaResponse consultarNombradaByConcesionaria(consultarNombradaByConcesionaria $parameters) [6] => verificarTrabajadorPortuarioResponse verificarTrabajadorPortuario(verificarTrabajadorPortuario $parameters) [7] => registrarNombradasResponse registrarNombradas(registrarNombradas $parameters) [8] => modificarNombradasResponse modificarNombradas(modificarNombradas $parameters) [9] => getResolucionNombradaResponse getResolucionNombrada(getResolucionNombrada $parameters) [10] => consultarNombradaResponse consultarNombrada(consultarNombrada $parameters) [11] => eliminarNombradaResponse eliminarNombrada(eliminarNombrada $parameters) [12] => consultarNombradaByConcesionariaResponse consultarNombradaByConcesionaria(consultarNombradaByConcesionaria $parameters) [13] => verificarTrabajadorPortuarioResponse verificarTrabajadorPortuario(verificarTrabajadorPortuario $parameters) )Ahora bien, digamos que quieres ejecutar una función como
consultarNombrada.Podrías hacerlo de esta forma:
$response = $client->getResolucionNombrada([ 'idNombrada' => 1, 'rutEmpresa' => 2 ]);
Pero, al correrlo obtendrás un error similar a:
SOAP Fault: Fault Code: soap:Server Fault String: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.NullReferenceException: Object reference not set to an instance of an object. at Services.Servicios.Nombradas.getResolucionNombrada(Int32 idNombrada, Int32 rutEmpresa) in C:\VSTS-Apl-Agent\_work\48\s\SCCNLP_Services\Services\Servicios\Nombradas.asmx.cs:line 144 --- End of inner exception stack trace ---
¿Qué ha ocurrido? ¿Es un problema del lado del webservice?
No. Simplemente, no configuraste la autenticación, por lo tanto, la llamada falla.
Si miras nuevamente el WSDL verás esta definición:
<soap:header message="tns:registrarNombradasUserCredentials" part="UserCredentials" use="literal"/>
Que indica que, como parte de la llamada, es necesario especificar un encabezado llamado
tns:registrarNombradasUserCredentialsEste elemento usa la definición que aparece al comienzo (En la parte de los tipos):
<s:complexType name="UserCredentials"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="userName" nillable="true" type="s:string"/> <s:element minOccurs="1" maxOccurs="1" name="password" nillable="true" type="s:string"/> </s:sequence> <s:anyAttribute/> </s:complexType>
Es decir que, para que la llamada tenga éxito, debes incluir la información de autenticación (
userNameypassword) como un encabezado SOAP.Puedes lograr esto agregando lo siguiente a tu código:
$client->__setSoapHeaders([ new SoapHeader( "https://sccnlp.com/", "UserCredentials", [ "userName" => "XXXX", "password" => "YYYY", ] ) ]);Y, asumiendo que tengas datos correctos para realizar la autenticación y que tu usuario tenga los permisos requeridos, obtendrás la información que buscas.
Algo interesante de hacer este ajuste de esta forma es que los datos de autenticación estarán disponibles para todas las subsiguientes llamadas al servicio.
-

Cómo usar PHPUnit
Te decidiste. Llegó la hora de incorporar el testing automatizado a tus proyectos.
Permitime que te felicite, es un gran paso hacia la generación de software de calidad superior.
Después de hacer la debida investigación hay pocas dudas: PHPUnit es la herramienta que debes conocer.
Para hacerte un poco más sencillo el camino, te dejo los lineamientos para dar tus primeros pasos.
Cómo instalar PHPUnit
Lo primero, como de costumbre, será instalar la herramienta.
Si vas al sitio de phpunit.de encontrarás algo como:

Nada mal para tener a mano pero, para empezar de cero… puede ser algo intimidante.
Antes de continuar, una advertencia: las versiones de phpUnit están bastante ligadas a las de php. Esto significa que, para asegurarte de elegir una versión que puedas utilizar, debes saber qué versión de php tienes instalada (O piensas instalar, por ejemplo, si utilizarás docker).
Seleccionar la versión de phpunit
Asumamos que, para arrancar, usarás el php instalado en tu propio host.
Un simple
php -valcanzará:
Pues bien, en este caso, la versión que más conviene utilizar es la 11, como puede verse aquí:

El siguiente paso: instalar la librería.
Instalar phpunit mediante el phar
La primera opción disponible es descargar el paquete cerrado (el archivo .phar) de aquí y hacerlo ejecutable.
Esta opción puede resultar útil si quieres dejar una única versión de phpunit disponible para todos tus proyectos.
Instalar phpunit usando composer
Por lejos, la forma que recomiendo (y utilizo) es hacerlo a través de composer.
De esta forma, la dependencia quedará circunscripta al proyecto en el que estás trabajando en este momento, a la vez que puedes compartir esta configuración con el resto de tu equipo.
Usa
composer require --dev phpunit/phpunit ^11y composer se encargará de todo.Para verificar tu instalación usa el comando
./vendor/bin/phpunit --versionSi ves algo como
PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
PHPUnit 11.4.4 by Sebastian Bergmann and contributors.Estás listo para avanzar.
Cómo escribir un test con PHPUnit
Escribir un test de PHPUnit supone crear una nueva clase que extienda de
TestCase:<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class MyTest extends TestCase { }Para ejecutar tus tests puedes utilizar el comando
./vendor/bin/phpunit MyTest.phpy obtendrás una salida como:PHPUnit 11.4.4 by Sebastian Bergmann and contributors. Runtime: PHP 8.4.1 There was 1 PHPUnit test runner warning: 1) No tests found in class "MyTest". No tests executed!
Bastante razonable ¿no? Al fin y al cabo, se ha definido un TestCase pero ningún test dentro. Corrijamos eso.
Por convención, PHPUnit entenderá que cualquier método de una clase TestCase cuyo nombre comience por
testdebe ser ejecutado por él, es decir, lo próximo que deberías hacer es agregar un método como este:<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class MyTest extends TestCase { public function testSomething(): void { } }Al ejecutar este test verás algo como:
PHPUnit 11.4.4 by Sebastian Bergmann and contributors. Runtime: PHP 8.4.1 R 1 / 1 (100%) Time: 00:00.003, Memory: 8.00 MB There was 1 risky test: 1) MyTest::testSomething This test did not perform any assertions /home/mauro/phpunit-poc/MyTest.php:6 OK, but there were issues! Tests: 1, Assertions: 0, Risky: 1.
Es decir, se ha ejecutado un test, sólo que este test no ha verificado nada, es decir, no hay realizado ningún assertion.
Para que efectivamente aporte algo de información, dentro del cuerpo del test debe utilizarse alguno de los métodos
assert*que provee phpUnit, por ejemplo:<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class MyTest extends TestCase { public function testSomething(): void { $this->assertTrue(true); } }Y ahora sí, llegarás a una salida algo más parecida a lo deseable:
PHPUnit 11.4.4 by Sebastian Bergmann and contributors. Runtime: PHP 8.4.1 . 1 / 1 (100%) Time: 00:00.003, Memory: 8.00 MB OK (1 test, 1 assertion)
Por supuesto que este test en la realidad no aporta mucho. Depende de vos hacer tests que sean relevantes para tu aplicación.
Ejemplo de un test con PHPUnit
Veamos un ejemplo algo más realista de lo que se puede hacer con phpUnit.
Imaginemos que tienes una función que, recibe una lista de números de teléfono y una lista de prefijos y retorna aquellos que comienzan por alguno de los prefijos indicados por el segundo argumento.
Un test para dicha función podría ser este:
<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class MyTest extends TestCase { public function testPhoneFilter(): void { $filteredPhoneNumbers = filter_phone_numbers( [ '(34) 665-55-22-112', '34 992 11 22 33', '+54 9 11 5494 2211', '054 121 123123' ], [ '34', '054' ], ); $this->assertEquals( [ '34 992 11 22 33', '054 121 123123'] , $filteredPhoneNumbers ); } }Por dónde continuar aprendiendo
PHPUnit ofrece una cantidad de posibilidades realmente interesante pero, para no marearte con todo al comienzo te recomiendo continuar aprendiendo sobre dobles de test y sobre la configuración de phpUnit.
Ya habrá tiempo para lo demás.
-

Cómo instalar extensiones PHP en Docker
Estás arrancando la dockerización de tu aplicación PHP.
Como para probar un poco, levantaste un contenedor usando un comando:
docker run -v $(pwd):/app -it php:latest index.phpY ahí nomás te encontraste con el primero de los problemas:
PHP Fatal error: Uncaught Error: Class "ZipArchive" not found
¡Claro!Tu aplicación necesita de la extensión Zip para funcionar… ¿Y ahora?
Bueno, a no desesperar.
Tenés varias opciones disponibles:
- Buscar una imagen que ya tenga la extensión zip instalada
- Agregar las instrucciones para que se compile e instale la extensión durante el build de la imagen
- Usar un script de instalación
Para el resto de los ejemplos tomaré una versión hiper simplificada de lo que puede ser tu aplicación:
<?php $zip = new ZipArchive(); if ($zip->open($argv[1]) == TRUE) { for ($i = 0; $i < $zip->numFiles; $i++) { echo $zip->getNameIndex($i).PHP_EOL; } }Y asumiré que tienes un archivo comprimido llamado
compressed.zip.Una imagen Docker PHP con la extensión zip
Seguramente habrá muchas opciones, una de ellas, como para salir del paso, es
thecodingmachine/php:7.3-v4-slim-cli(Podés encontrar otras similares acá).Para usarla este comando puede ser útil:
docker run -v $(pwd):/usr/src/app --rm -it thecodingmachine/php:7.3-v4-slim-cli php index.php compressed.zipComo decía, esta solución no es ni de lejos la mejor.
¿Por qué? Básicamente porque tenés muy poco control sobre lo que tiene la imágen. En otras palabras, no sabés muy bien qué otras cosas te estás trayendo además de la extensión zip.
En el mejor de los casos vas a terminar con una imagen más grande de lo necesario.
En el peor vas a incluir extensiones que no necesitás y exponerte a riesgos de seguridad.
Mejor seguir leyendo.
Compilar la extensión zip dentro del build de la imagen
Una forma de contar con la extensión zip (o cualquier otra en realidad) en tu contenedor es compilar php de modo que lo tenga incorporado.
Por ejemplo, usando un Dockerfile como este:
FROM ubuntu:latest ADD https://www.php.net/distributions/php-8.4.1.tar.gz /php/php.tar.gz RUN apt-get update && \ apt-get install -y libzip-dev build-essential pkg-config libxml2-dev libsqlite3-dev && \ tar xzf /php/php.tar.gz -C /php/ && \ cd /php/php-8.4.1/ && \ ./configure --prefix=/usr/local/php-8.4.1 --with-zip && \ make && make install && \ ln -s /usr/local/php-8.4.1/bin/php /usr/local/bin WORKDIR /appY construtendo la imagen usando
docker build . -t php-zipPodrías ejecutar tu script usando:
docker run -v $(pwd):/app --rm -it php-zip php index.php compressed.zipY no verás el error que estaba al comienzo.
Esta opción, si bien es más correcta que la primera, puede resultar un poco overkill. Particularmente, si tenés que instalar varias extensiones, llegar al Dockerfile correcto puede ser bastante laborioso.
En la siguiente sección te doy la que personalmente recomiendo
Un script de instalación de extensiones PHP para Docker
Bueno, en realidad no es un script de instalación si no dos.
Arranco por el más conocido:
php-ext-install.Lo bueno de este script es que, si partís de una imagen oficial de php, no tenés que hacer nada para tenerlo disponible.
Lo malo de este script es que requiere bastante ayuda para hacer lo suyo.
Por ejemplo, instalar la extensión zip implica agregar a tu
Dockerfile:RUN docker-php-ext-install zipPero… con eso sólo no alcanza. Si lo intentas, al hacer el build verás este error:
Package 'libzip', required by 'virtual:world', not found
Package 'libzip', required by 'virtual:world', not found
Package 'libzip', required by 'virtual:world', not foundLo que quiere decir que necesitas, antes de esto, instalar la librería zip.
En definitiva, el
Dockerfileque te dará lo que buscas es uno que se parezca a:FROM php:8.4.1-cli RUN apt update && apt-get install -y libzip-dev && docker-php-ext-install zip
En este caso no ha sido tan terrible, cierto, pero hay otras extensiones que también requieren ciertos cambios en configuraciones. Nada del otro mundo pero es algo más que hay que recordar.
El script que me gusta más, y el que recomiendo usar, es
docker-php-extension-installer.A diferencia del primero,
docker-php-extension-installerdebe ser instalado explícitamente pero, una vez instalado, es mucho más cómodo.Aquí un ejemplo de
Dockerfilepara la extensión zip:FROM php:8.4.1-cli ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ RUN install-php-extensions zip WORKDIR /app
Nuevamente, ejecutando
docker run -v $(pwd):/app --rm -it php-zip php index.php compressed.zipverás el listado de los archivos contenidos en el archivo comprimido.Así que ya lo sabés: la próxima vez que te toque agregar extensiones a un php sobre docker, agregá esta línea a tu Dockerfile:
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
Y problema resuelto.
-

Cómo agregar una página de error 500 en un proyecto PHP
Es muy feo encontrarse con una página tipo

Cuando estás intentando encontrar lo que buscás, ¿cierto?
Es esa sensación de impotencia sobre todo.
Error 500.
Error interno del servidor.
Es la versión digital del «No sos vos, soy yo»…
O peor, algo como:

Ahí directamente te sentirías insultado…
Imaginate que lo mismo sienten los visitantes de tus sitios. O, más probablement, los visitantes de los sitios de tus clientes.
Claro que, por más que intentes, tener un sistema que no falle nunca jamás es bastante difícil. Especialmente cuando éste se haya en las versiones preliminares.
Pero, si bien no es práctico pasar días o meses retrasando la salida a producción hasta estar 120% seguro de que no hay ningún caso de borde escondido acechando detrás de la puerta, algo que va muy bien es hacer de la experiencia de encontrarse ante un error algo menos traumático.
Dependiendo del contexto podés ofrecer algo que cause gracia como:
Pero, más allá de la elegancia de este tratamiento de errores, hay algo muy importante: no dejar al visitante encerrado en un callejón sin salida.
Notá como, en el último ejemplo se preserva el menú desplegable, el link a volver a la página principal y la señal de notificaciones:

Definitivamente, una mejor experiencia, ¿cierto?
La pregunta infaltable: ¿Cómo podrías tener esto en tus aplicaciones?
Veamos algunas alternativas.
Configurar una página de error a través del webserver
La primera forma de lograr esto es usando la configuración del webserver.
La idea es que sea él quien, dependiendo del código HTTP que retorne tu aplicación, redirija el tráfico a un script definido a tal efecto.
Versión Apache
Si tu servidor es Apache podés usar un archivo
.htaccesso, si tenés acceso directo a la configuración, hacerlo en la definición de tu VirtualHost.Se trata de usar la directiva
ErrorDocument, por ejemplo:ErrorDocument 500 http://tudominio.com/errors/500.phpDe este modo, cuando tu script principal retorne un código 500 el visitante será redirigido automáticamente al script
errors/500.phpy, una vez ahí, podés ponerle toda la estética de tu sitio, como si fuera una página más.Versión NginX
En el caso de NginX la herramienta a usar será
error_page:error_page 500 /errors/500.php
Esta puede ser una solución aceptable, aunque muchas veces es mejor dejar la infraestructura lo más simple posible y manejar toda la funcionalidad desde la aplicación.
Configurar un error handler en php
PHP ofrece una herramienta muy interesante para estos casos: la función
set_error_handlerMediante esta función es posible establecer tu propio callback, el cual será invocado cuando se produzca un error.
Por ejemplo si tuvieras un código como:
<?php trigger_error("Este es un error personalizado");Al ejecutarlo verías esta salida:
PHP Notice: Este es un error personalizado in /home/mauro/error_handler.php on line 3
Al agregarle el error_handler:
<?php set_error_handler(function( int $errno, string $errstr, ?string $errfile, ?int $errline, array $errcontext = []) : bool { echo "Se produjo el error $errno: $errstr"; return true; }); trigger_error("Este es un error personalizado");Y ejecutarlo verías:
Se produjo el error 1024: Este es un error personalizado
Es cierto, la diferencia no es grandiosa… pero eso depende de lo que pongas en el error_handler. Podrías enviar correos, enviar eventos a una cola o incluso, y esta es la parte más interesante, a diferencia de la opción anterior, generar páginas de error específicas para cada situación.
Otro detalle importante: la definición del error_handler deberías tomarla como algo similar al autoload. Es decir, si tu aplicación abarca muchos scripts, esta definición debería realizarse una única vez, en el que actúa como punto de entrada, probablemente
index.php. -

¿Cuántos contenedores necesita tu php?
Cuando trabajabas con máquinas virtuales no había dudas: cuanto más completa sea la máquina mejor.
Sí, instalarla por primera vez era un trabajito. Que definir el hardware, el disco, el sistema operativo, instalarl Apache, MySQL, Git… una mañana se te iba en un abrir y cerrar de ojos. Pero funcionaba.
Luego apareció Vagrant y fue como tocar el cielo con las manos… por un tiempo.
Ahora te estás queriendo pasar a Docker y el saber popular dice que hay que tener muchos contenedores pequeños en lugar de un único monolito que tenga todo lo necesario para correr la app.
¿Por qué esa es la mejor opción?
Dejame que te plantee la pregunta al revés: ¿por qué es una buena idea meter todo en una única máquina virtual?
Tal vez te parezca ridículo pero seguime el juego por un momento.
La respuesta es obvia, ¿no?: ¡porque te quedás sin máquina en un segundo!
Ahora bien, si tuvieras memoria infinita y un procesador que no se agota nunca… ¿no preferirías tener una VM por cada proceso?
Imaginate, un Apache en una VM, el MySQL en otra, tal vez un Redis por allá, una VM para correr los cronjobs…
Tan mal no estaría, ¿verdad?
Bueno, tal vez sería compleja la orquestación de todo eso, pero tampoco sería tan terrible.
De esa forma, cada VM podría tener su propio sistema operativo, sus propias dependencias instaladas y, en caso de que algo falle, sería sólo ese servicio el que se vería afectado.
De pronto no suena tan mal, ¿o sí?
Precisamente esa es la idea de usar Docker (O, más correctamente, contenedores): tener diferentes ambientes de ejecución auto-contenidos.
Pero… ¿no vas a caer en el mismo problema de correr muchas VMs en una misma computadora?
No. Acá es donde la cosa se pone más técnica pero digamos que la diferencia principal entre una VM y un contenedor es que la VM virtualiza hardware mientras que el contenedor sólo virtualiza software.
En la práctica, esto quiere decir que el contenedor es mucho más liviano que una VM.
La contracara es que el contenedor te da algo menos de flexibilidad.
Por ejemplo, es muy fácil tener corriendo una VM con Windows y otra con Linux en un host Mac. Hacer lo mismo con contenedores… no tanto.
Ahora bien, si tus contenedores usan el mismo sistema operativo de base (Por ejemplo son todos Linux), en un mismo host podés albergar significativamente más contenedores que VMs.
La siguiente pregunta es… administrar tantos contenedores ¿no se vuelve complejo?
Y… la respuesta es que un poco sí.
Ahí es donde aparece docker-compose para salvar el día. Tema para otro post.
-
Cómo leer un archivo XML con namespaces
Estoy tratando de leer un xml el cual contiene namespaces, que pesadilla 🙁.
La verdad es bastante confuso para mi ese tema; he tratado de varias maneras pero hasta ahora sin éxito, por ahí en la web nos indicaron que lo mas conveniente es hacerlo mediante DOM, estuve leyendo la documentación en el sitio oficial de php, pero en serio que no logro ni siquiera ingresar al nodo principal.
Me recomendaron usar la librería SimpleXMLElement, pero es peor, siempre devuelve null.
¿Tu historia se parece a esta? No estás solo.
Personalmente, el formato XML me ha dado más dolores de cabeza de los que me gusta recordar… de hecho, creo que quien lo inventó debía ser un poco sádico.
Pero bueno… lamentablemente, aún hay quienes lo usan así que… será mejor intentar amigarse con él ¿no?
Voy a intentar ilustrar la solución a este problema usando un ejemplo real.
Imaginá que recibiste un texto como este:
<?xml version="1.0" encoding="UTF-8"?> <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> <s:Header> <a:Action s:mustUnderstand="1">http://wcf.dian.colombia/IWcfDianCustomerServices/GetStatusZipResponse</a:Action> <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1"> <u:Timestamp u:Id="_0"> <u:Created>2022-07-16T21:22:34.242Z</u:Created> <u:Expires>2022-07-16T21:27:34.242Z</u:Expires> </u:Timestamp> </o:Security> </s:Header> <s:Body> <GetStatusZipResponse xmlns="http://wcf.dian.colombia"> <GetStatusZipResult xmlns:b="http://schemas.datacontract.org/2004/07/DianResponse" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <b:DianResponse> <b:ErrorMessage xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> <c:string>Regla: FAJ41, Notificación: El contenido de este elemento no corresponde al nombre y código valido.</c:string> <c:string>Regla: FAJ73, Notificación: Estructura código no valida</c:string> </b:ErrorMessage> <b:IsValid>true</b:IsValid> <b:StatusCode>00</b:StatusCode> <b:StatusDescription>Procesado Correctamente.</b:StatusDescription> <b:StatusMessage>La Factura electrónica SETP-99111223, ha sido autorizada.</b:StatusMessage> <b:XmlBase64Bytes></b:XmlBase64Bytes> <b:XmlBytes i:nil="true"/> <b:XmlDocumentKey></b:XmlDocumentKey> <b:XmlFileName>face_f0900056122003b023380</b:XmlFileName> </b:DianResponse> </GetStatusZipResult> </GetStatusZipResponse> </s:Body> </s:Envelope>Y necesitás obtener la información de las etiquetas:
<u:Created><u:Expires><b:ErrorMessage xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays"><b:XmlDocumentKey><b:XmlFileName>
¿Cómo podrías hacer?
El elefante en la habitación
Este xml no es cualquier xml. Se trata de parte de una respuesta emitida por un webservice SOAP con lo cual, probablemente la mejor opción para resolver este problema sea evitarlo directamente, es decir, apoyarte en la clase SOAPClient.
De hecho, si lo miras con un poco más de detenimiento, se trata de una factura electrónica generada para Colombia. Si tu problema se parece a ese tal vez te convenga revisar este artículo.
Dicho esto, supongamos que, por alguna razón, esa opción no está disponible y no queda otra que interpretar a mano el xml.
Recargá el café y comencemos.
Usando xpath
Una primera aproximación a esto sería usar un código tipo:
<?php $xml = new SimpleXMLElement($xmlString); $created = $xml->xpath("//s:Envelope/s:Header/o:Security/u:Timestamp/u:Created"); print_r($created);Pero al ejecutarlo vas a obtener algo como:
Warning: SimpleXMLElement::xpath(): Undefined namespace prefix in /app/index.php on line 3
¿Qué pasó?
El problema es que
xpathno conoce el mapeo de prefijos (s:,o:yu:) a las respectivas definiciones de namespaces y, por lo tanto, no será capaz de interpretar el texto del XML.Una solución posible para esto es ayudar un poco al pobre
xpath.¿Cómo?
Utilizando el método
registerXPathNamespacedel siguiente modo:<?php $xml = new SimpleXMLElement($xmlString); $xml->registerXPathNamespace('s', "http://www.w3.org/2003/05/soap-envelope"); $xml->registerXPathNamespace('o', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); $xml->registerXPathNamespace('u', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); $created = $xml->xpath("//s:Envelope/s:Header/o:Security/u:Timestamp/u:Created"); print_r($created);De esa forma, al ejecutar el programa verás:
Array ( [0] => SimpleXMLElement Object ( [0] => 2022-07-16T21:22:34.242Z ) )Y, a partir de ahí, si lo que efectivamente quieres es obtener la fecha puedes valerte de la clase DateTime o, su versión inmutable, DateTimeImmutable:
<?php $xml = new SimpleXMLElement($xmlString); $xml->registerXPathNamespace('s', 'http://www.w3.org/2003/05/soap-envelope'); $xml->registerXPathNamespace('o', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); $xml->registerXPathNamespace('u', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); $createdValue = $xml->xpath("//s:Envelope/s:Header/o:Security/u:Timestamp/u:Created")[0][0]->__toString(); try { $createdDate = new DateTimeImmutable($createdValue); echo "Created at: ".$createdDate->format("D M d H:i:s O").PHP_EOL; } catch (DateMalformedStringException $e) { echo $e->getMessage().PHP_EOL; }Usando las propiedades del objeto SimpleXMLElement
En esta versión, en lugar de ir por el xpath lo haremos ingresando directamente a las propiedades del objeto SimpleXMLElement.
Cada uno de los elementos definidos en tags hijos del elemento principal (
<s:envelope>en este caso) se convierte en forma automática, en una propiedad de sus elementos hijos.Normalmente lo que haríamos sería:
<?php $xml = new SimpleXMLElement($xmlString); $createdValue = $xml ->children() ->Header ->children() ->Security ->children() ->Timestamp ->children() ->Created ->__toString(); try { $createdDate = new DateTimeImmutable($createdValue); echo "Created at: " . $createdDate->format("D M d H:i:s O") . PHP_EOL; } catch (DateMalformedStringException $e) { echo $e->getMessage() . PHP_EOL; }Pero… nuevamente nos encontramos con un fallo:
Warning: Attempt to read property "Security" on null in /app/index.php on line 53 Fatal error: Uncaught Error: Call to a member function children() on null in /app/index.php:54
El problema aquí es que la identificación de esos hijos también depende del correcto mapeo del Namespace a su definición, con lo cual, es necesario indicar este hecho para que la navegación funcione
<?php $xml = new SimpleXMLElement($xmlString); $createdValue = $xml ->children("s", true) ->Header ->children("o", true) ->Security ->children("u", true) ->Timestamp ->children("u", true) ->Created ->__toString(); try { $createdDate = new DateTimeImmutable($createdValue); echo "Created at: " . $createdDate->format("D M d H:i:s O") . PHP_EOL; } catch (DateMalformedStringException $e) { echo $e->getMessage() . PHP_EOL; }Namespaces y XML
En conclusión, trabajar con namespaces y XML no es tan complejo como puede parecer. Bueno, tan no complejo como XML permite al menos.
Sólo se trata de tener en cuenta cómo funcionan los namespaces y darle un poquito de guía a la librería
SimpleXMLElementpara que pueda hacer su magia como siempre. -
Cómo testear una aplicación que depende de una API sin conexión a Internet
Venís tirando código como un campeón.
Las integraciones con las APIs de Twitter y Google no esconden ningún secreto.
Sólo te falta probar el último feature.
Le das a correr y…
Timeout.
Lo qué??
Revisás el código de arriba abajo… ¿Cómo es posible?
Si hace 5 minutos funcionaba de 10…
Levantás la vista y ves como el router te sonríe con sorna:

Como diciéndote: A ver cuándo vas a aprender quién manda acá.
¿Otra vez se cayó Internet?
¿De nuevo vas a tener que llamar al proveedor para que lo reparen de una buena vez?
Y… otra no queda… si hay que testear la API… se necesita conectividad…
¿O no?
Y de pronto… muy bajito en la distancia se escucha la voz del maestro Yoda susurrando «Tests escribir debes».
Exacto. Si querés tener un entorno sólido de desarrollo tenés que aislarte de las dependencias de terceros.
Cómo escribir tests que no dependan de terceros
Las soluciones son múltiples y mucho dependerá del contexto pero todas siguen la misma lógica: en vez de depender de un servicio poco confiable (O al menos, uno que está fuera de tu control), usá un doble del cual tengas control total.
Usar un doble de test a nivel unitario
Empecemos por un ejemplo simple a nivel de test unitario.
Imaginemos que tenés un código que usa una librería como esta y es algo del estilo de:
<?php declare(strict_types=1); namespace App; use Exception; use GuzzleHttpExceptionGuzzleException; use NowehTwitterApiClient; readonly class TwitterMonitor { public function __construct(private Client $client) { } /** * @return array<Mention> * @throws Exception * @throws GuzzleException */ public function getRecentMentions(string $uid): array { $mentions = $this->client ->timeline() ->getRecentMentions($uid) ->performRequest() ; foreach ($mentions->data as $mention) { $return[] = new Mention($mention); } return $return; } }Está claro que, si
$client->performRequest()falla, por ejemplo por falta de conectividad, todo lo que sigue va a fallar.Algo que podrías hacer en tu test es usar un mock. Sería algo así como:
<?php declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; use App\Mention; use App\TwitterMonitor; use GuzzleHttp\Exception\GuzzleException; use Noweh\TwitterApi\Timeline; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Noweh\TwitterApi\Client; class TwitterMonitorShould extends TestCase { const string UID = "1346889436626259968"; private static function a2stdClass(array $array): stdClass { return json_decode(json_encode($array)); } /** * @throws Exception|GuzzleException */ #[Test] public function returnUsersLastMentions(): void { $timeline = $this ->getMockBuilder(Timeline::class) ->disableOriginalConstructor() ->getMock(); $timeline ->expects($this->once()) ->method('getRecentMentions') ->with(self::UID) ->willReturn($timeline); $mentionsData = self::a2stdClass([ 'data' => [ [ "author_id" => "2244994945", "created_at" => "Wed Jan 06 18:40:40 +0000 2021", "id" => "1346889436626259968", "text" => "Learn how to use the user Tweet timeline and user mention timeline endpoints in the X API v2 to explore Tweet\\u2026 https:\\/\\/t.co\\/56a0vZUx7i", "username" => "XDevelopers" ] ] ]); $timeline ->expects($this->once()) ->method('performRequest') ->willReturn($mentionsData); $client = $this ->getMockBuilder(Client::class) ->disableOriginalConstructor() ->getMock(); $client ->expects($this->once()) ->method('timeline') ->willReturn($timeline); $userMonitor = new TwitterMonitor($client); $this->assertEquals([new Mention($mentionsData->data[0]),], $userMonitor->getRecentMentions(self::UID)); } }De esta forma, cuando ejecutes tus tests con phpUnit la llamada a Twitter no se realizará, si no que será el doble quien responda con la respuesta pre-armada.
Usar un servicio mockeado
Esta técnica puede ser algo más compleja de implementar pero probablemente sea mejor si lo que buscás es hacer algún tipo de test de caja negra.
La idea aquí es «engañar» a tu aplicación para que crea que está hablando con el servicio real cuando, en realidad, está interactuando con uno local.
Si no querés montarte tu propio servicio, podés usar una herramienta como mockoon, la idea es que puedas hacerle peticiones como si estuvieras yendo contra el servicio real.
Es importante comprender que lo que podés testear es aquello que controlás, para todo lo demás tenés que asumir que el comportamiento esperado es el real. En otras palabras: dado que los terceros hacen su parte como se espera, tu aplicación debe comportarse como se espera, en caso de que no se cumpla la premisa no estás obligado a dar garantías.
Obviamente, te conviene dar garantías de que tu aplicación nunca va a generar daños pero esa es otra historia.
Veamos cómo sería en este caso.
Ante todo, tenemos que averiguar a qué URL estamos haciendo los requests. Esta parte no es muy compleja: se trata de seguir la cadena de llamadas hacia atrás:
Empezamos por
Client::timeline()donde nos encontramos con que la respuesta es un objetoTimeline, el cual será el que eventualmente ejecutará el métodoperformRequest.Entramos un poco más y nos encontramos con:
if ($this->auth_mode === 0) { // Bearer Token // Inject the Bearer token header $client = new Client(['base_uri' => self::API_BASE_URI]); $headers['Authorization'] = 'Bearer ' . $this->bearer_token; } elseif ($this->auth_mode === 1) { // OAuth 1.0a User Context // Insert Oauth1 middleware $stack = HandlerStack::create(); $middleware = new Oauth1([ 'consumer_key' => $this->consumer_key, 'consumer_secret' => $this->consumer_secret, 'token' => $this->access_token, 'token_secret' => $this->access_token_secret, ]); $stack->push($middleware); $client = new Client([ 'base_uri' => self::API_BASE_URI, 'handler' => $stack, 'auth' => 'oauth' ]); } else { // OAuth 2.0 Authorization Code Flow throw new RuntimeException('OAuth 2.0 Authorization Code Flow had not been implemented & also requires user interaction.'); }Es decir: la pieza clave aquí es la constante
AbstractController::API_BASE_URI, cuya definición no es otra quehttps://api.twitter.com/2/… qué sorpresa, ¿no?Vamos a usar este dato para configurar nuestro proxy de modo de que la aplicación jamás se entere de que está hablando con un doble y no con el verdadero Twitter.
Descargar la especificación de OpenAPI de la documentación de Twitter es un buen punto de partida.
El siguiente paso es importarla en Mockoon:

Con esto estarás listo para tener un servidor mockeado para empezar a jugar

Por ejemplo, podrías cambiar el contenido de la respuesta que generó Mockoon para que retorne:
"data": [ { "author_id": "22123122345", "created_at": "Wed Oct 23 18:10:20 +0000 2024", "id": "2346889456626258962", "text": "Check out this article by @mchojrin!", "username": "PHP4Ever" } ],Y probar tu aplicación vía CLI o web, depende del caso a ver si está levantando esta respuesta.
Bueno… en realidad todavía vas a tener un pequeño inconveniente en este caso en particular: la librería tiene la URL de Twitter hardcodeada y vos necesitás que Mockoon atrape todos esos requests.
Hay varias soluciones que podés tomar para esto, las nombro solamente para no irme demasiado del tema de este post, en todo caso, escribiré algún otro detallando:
- Cambiar el código de la librería para que apunte, en lugar de a
twitter.comalocalhost:3000(La mala) - Montar un proxy en tu computadora que redirija el tráfico destinado a
twitter.comhacialocalhost:3000(La fea) - Derivar una clase de la que te propone la librería y hacer que la URL base sea un parámetro del constructor (La buena)
Más allá de cuál sea la solución que tomes, con este esquema vas a poder testear End-to-End sin depender de la conectividad.
El lado oscuro de los dobles
Si bien estas técnicas son muy útiles para trabajar tranquilo, dependen de un hecho fundamental: las respuestas hechas a mano deben coincidir (en estructura al menos) con aquellas devueltas por el servicio real.
Si bien esto puede parecer obvio, es importante recordarlo porque muchas veces la propia documentación de la API contra la que estás trabajando está desactualizada y, en ese caso… nada más cierto que la realidad misma.
- Cambiar el código de la librería para que apunte, en lugar de a
-

Cómo actualizar la versión de php que tiene una imagen docker
¿Alguna vez te pasó algo como esto?
…me he bajado una imagen que contiene wordpress con letsencrypt, pero la versión de php que utiliza es la 5.6 y necesito actualizarla a la 7.2.
¿Hay alguna forma de modificar esto desde dentro o desde fuera del contenedor?Qué dilema, ¿no?
La primera pregunta que se me ocurre es ¿realmente te bajaste una imágen y no un
Dockerfile?Por un rato asumiré que así es… después lo volvemos a revisar.
Así que, teniendo esta imagen podés construir contenedores pero… ¿si hacés la actualización y el contenedor se destruye? ¡Olvidate de la actualización! No suena muy divertido, ¿no? ¿Y entonces? ¿¿No hay salida??
Bueno… no tan rápido.
Un comando de Docker no muy usado (ni muy comentado honestamente) es el comando commit el cual puede venir bastante bien en esta situación.
Este comando permite crear una imagen basada en el estado actual de un contenedor. Es decir, es una forma de crear una imagen sin tener un Dockerfile.
En este hipotético escenario se podría hacer algo del estilo:
docker run -it wordpress:php5.6 bashY, una vez dentro, realizar la actualización siguiendo algún tutorial como este por ejemplo.
El problema, o mejor dicho, el primero de los problemas, es que esta versión de php es tan vieja que las imágenes que la contienen suelen estar basadas en versiones igual de viejas el sistema operativo. Esto implica que los repositorios ya quedaron obsoletos… en fin, que hacer esta actualización será un viajecito en sí mismo (Sí, te lo digo por experiencia después de haber probado unas 10 imágenes tratando de armar un ejemplo para este post).
Pero bueno… supongamos que, de alguna forma, te las arreglaste para actualizar el php.
Mientras el contenedor no se elimine, podrás usar
docker ps -apara obtener algo como:
Y, una vez obtenido el ID del contenedor:
docker commit 9d50869ba044 wordpress:php5.6.mucEsto te creará una nueva imagen para usar en tu sistema con la cual podrás crear un nuevo contenedor, por ejemplo:
docker run -it wordpress:php5.6.muc bashY ahora sí, al hacer
php -vencontrarás la versión de php que necesitabas.¡Genial! ¿No? Bueno… no exactamente.
Para empezar, la nueva versión de php no necesariamente será compatible con tu versión de WordPress… es más, lo más probable es que ese no sea el caso.
Aún si lo fuera… ¿vale la pena ponerse a actualizar la versión de PHP? La verdad es que lo veo bastante poco práctico.
Precisamente, la idea de usar Docker es crear contenedores desechables. ¿Quedó desactualizada la versión de php? Ningún problema, armemos una nueva imagen con la versión que se se requiere y listo.
De hecho, lo más probable es que lo que hayas descargado sea un Dockerfile más que una mera imagen.
Si ese es el caso, podrías hacer algo tan sencillo como modificar la imagen usada como base y re-construir la imagen.
Es decir, si el Dockerfile se ve algo así como:
# Base image FROM php:5.6-apache # Install the mysqli extension RUN docker-php-ext-install mysqli # Update repo RUN apt-get update -y # Install mysql-client RUN apt-get install mysql-client -y # Install wp-cli as wp RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \ chmod +x wp-cli.phar && \ mv wp-cli.phar /usr/local/bin/wp # Download and extract WordPress files on /var/www/html RUN wp core download --allow-root # Get args from docker-composer.yml ARG WORDPRESS_DB_NAME ARG WORDPRESS_DB_USER ARG WORDPRESS_DB_PASSWORD ARG WORDPRESS_DB_HOST # Creating wp-config.php file on /var/www/html RUN wp config create --dbname=${WORDPRESS_DB_NAME} --dbuser=${WORDPRESS_DB_USER} --dbpass=${WORDPRESS_DB_PASSWORD} --dbhost=${WORDPRESS_DB_HOST} --allow-root --skip-check COPY entrypoint.sh /entrypoint.sh # makes the script executable RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["apache2-foreground"]Podrías cambiarlo por
# Base image FROM php:7.2-apache # Install the mysqli extension RUN docker-php-ext-install mysqli # Update repo RUN apt-get update -y # Install mysql-client RUN apt-get install mysql-client -y # Install wp-cli as wp RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \ chmod +x wp-cli.phar && \ mv wp-cli.phar /usr/local/bin/wp # Download and extract WordPress files on /var/www/html RUN wp core download --allow-root # Get args from docker-composer.yml ARG WORDPRESS_DB_NAME ARG WORDPRESS_DB_USER ARG WORDPRESS_DB_PASSWORD ARG WORDPRESS_DB_HOST # Creating wp-config.php file on /var/www/html RUN wp config create --dbname=${WORDPRESS_DB_NAME} --dbuser=${WORDPRESS_DB_USER} --dbpass=${WORDPRESS_DB_PASSWORD} --dbhost=${WORDPRESS_DB_HOST} --allow-root --skip-check COPY entrypoint.sh /entrypoint.sh # makes the script executable RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["apache2-foreground"]Y ejecutar nuevamente
docker build.Ojo: ¡Esto no resuelve el problema de la incompatibilidad de la versión de PHP y de WordPress! Pero al menos te ahorra unos cuantos dolores de cabeza tratando de hacer la migración.
Mi consejo al final sería pasar todos los datos a volúmenes, buscar una imagen que tenga las versiones del software que querés usar y con esa imagen crear un nuevo contenedor donde montarlos.
Probablemente eso sea mucho más sencillo y eficiente.